fix(manacore): improve dashboard layout polish

- Remove unnecessary wrapper div in WidgetContainer
- Increase grid gap from gap-4 to gap-5 for breathing room
- Add auto-rows-fr for equal row heights
- Add min-h on widget content so empty widgets aren't tiny
- Change default layout to 3 equal columns (small)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-24 12:21:51 +01:00
parent 02215dfb12
commit 61c23d5e79
45 changed files with 185 additions and 100 deletions

View file

@ -94,9 +94,7 @@ export class AchievementService implements OnModuleInit {
}
async getStats(userId: string): Promise<{ total: number; unlocked: number }> {
const [totalResult] = await this.db
.select({ count: sql<number>`count(*)` })
.from(achievements);
const [totalResult] = await this.db.select({ count: sql<number>`count(*)` }).from(achievements);
const [unlockedResult] = await this.db
.select({ count: sql<number>`count(*)` })
@ -113,7 +111,10 @@ export class AchievementService implements OnModuleInit {
* Check all achievements for a user and unlock any newly earned ones.
* Called after XP gain, skill creation, activity logging, etc.
*/
async checkAndUnlock(userId: string, context?: { activityXp?: number }): Promise<AchievementUnlockResult[]> {
async checkAndUnlock(
userId: string,
context?: { activityXp?: number }
): Promise<AchievementUnlockResult[]> {
const allAchievements = await this.db.select().from(achievements);
const unlocked = await this.db
.select()

View file

@ -22,11 +22,15 @@ function calculateLevel(xp: number): number {
export class SkillService {
constructor(
@Inject(DATABASE_TOKEN) private db: Database,
private readonly achievementService: AchievementService,
private readonly achievementService: AchievementService
) {}
async findAll(userId: string): Promise<Skill[]> {
return this.db.select().from(skills).where(eq(skills.userId, userId)).orderBy(desc(skills.totalXp));
return this.db
.select()
.from(skills)
.where(eq(skills.userId, userId))
.orderBy(desc(skills.totalXp));
}
async findByBranch(userId: string, branch: string): Promise<Skill[]> {
@ -109,7 +113,12 @@ export class SkillService {
id: string,
userId: string,
dto: AddXpDto
): Promise<{ skill: Skill; leveledUp: boolean; newLevel: number; newAchievements: AchievementUnlockResult[] }> {
): Promise<{
skill: Skill;
leveledUp: boolean;
newLevel: number;
newAchievements: AchievementUnlockResult[];
}> {
const skill = await this.findByIdOrThrow(id, userId);
const newTotalXp = skill.totalXp + dto.xp;

View file

@ -33,6 +33,7 @@ COPY packages/shared-utils ./packages/shared-utils
COPY packages/shared-error-tracking ./packages/shared-error-tracking
COPY packages/shared-vite-config ./packages/shared-vite-config
COPY packages/shared-api-client ./packages/shared-api-client
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
# Copy skilltree web
COPY apps/skilltree/apps/web ./apps/skilltree/apps/web

View file

@ -74,7 +74,11 @@
<!-- XP reward + unlock date -->
<div class="mt-2 flex items-center gap-3 text-xs">
<span class="flex items-center gap-1 {achievement.unlocked ? 'text-yellow-400' : 'text-gray-500'}">
<span
class="flex items-center gap-1 {achievement.unlocked
? 'text-yellow-400'
: 'text-gray-500'}"
>
<Star class="h-3 w-3" />
+{achievement.xpReward} XP
</span>

View file

@ -59,7 +59,9 @@
</div>
<!-- Achievement unlocked text -->
<h2 class="mb-1 text-2xl font-bold text-yellow-400 achievement-text">Achievement freigeschaltet!</h2>
<h2 class="mb-1 text-2xl font-bold text-yellow-400 achievement-text">
Achievement freigeschaltet!
</h2>
<!-- Achievement name -->
<p class="mb-2 text-xl font-semibold text-white">{result.achievement.name}</p>

View file

@ -66,7 +66,10 @@
</div>
<!-- Achievements -->
<a href="/achievements" class="rounded-xl border border-gray-700 bg-gray-800/50 p-4 transition-colors hover:border-yellow-600/50 hover:bg-yellow-900/10">
<a
href="/achievements"
class="rounded-xl border border-gray-700 bg-gray-800/50 p-4 transition-colors hover:border-yellow-600/50 hover:bg-yellow-900/10"
>
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-yellow-500/20">
<Medal class="h-6 w-6 text-yellow-500" />
@ -74,7 +77,9 @@
<div>
<p class="text-sm text-gray-400">Achievements</p>
<p class="text-2xl font-bold text-white">
{achievementStore.stats().unlocked}<span class="text-sm font-normal text-gray-500">/{achievementStore.stats().total}</span>
{achievementStore.stats().unlocked}<span class="text-sm font-normal text-gray-500"
>/{achievementStore.stats().total}</span
>
</p>
</div>
</div>

View file

@ -257,7 +257,9 @@ export async function saveAchievement(achievement: AchievementWithStatus): Promi
await db.put('achievements', achievement);
}
export async function saveAllAchievements(achievementsList: AchievementWithStatus[]): Promise<void> {
export async function saveAllAchievements(
achievementsList: AchievementWithStatus[]
): Promise<void> {
const db = await getDB();
const tx = db.transaction('achievements', 'readwrite');
await tx.objectStore('achievements').clear();

View file

@ -199,22 +199,14 @@ async function checkLocal(context: {
unlockedAt: new Date().toISOString(),
progress: condition.threshold,
};
achievements = [
...achievements.slice(0, i),
unlocked,
...achievements.slice(i + 1),
];
achievements = [...achievements.slice(0, i), unlocked, ...achievements.slice(i + 1)];
await storage.saveAchievement(unlocked);
newlyUnlocked.push({ achievement: a, xpReward: a.xpReward });
} else {
// Update progress
const updated = { ...a, progress: Math.min(current, condition.threshold) };
if (updated.progress !== a.progress) {
achievements = [
...achievements.slice(0, i),
updated,
...achievements.slice(i + 1),
];
achievements = [...achievements.slice(0, i), updated, ...achievements.slice(i + 1)];
await storage.saveAchievement(updated);
}
}

View file

@ -159,7 +159,9 @@
>
<Trophy class="h-5 w-5" />
{#if achievementStore.stats().unlocked > 0}
<span class="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-yellow-500 text-[10px] font-bold text-gray-900">
<span
class="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-yellow-500 text-[10px] font-bold text-gray-900"
>
{achievementStore.stats().unlocked}
</span>
{/if}
@ -359,8 +361,5 @@
{/if}
{#if showAchievementCelebration && currentAchievementUnlock}
<AchievementCelebration
result={currentAchievementUnlock}
onClose={showNextAchievement}
/>
<AchievementCelebration result={currentAchievementUnlock} onClose={showNextAchievement} />
{/if}

View file

@ -3,11 +3,7 @@
import { ACHIEVEMENT_CATEGORY_INFO, RARITY_INFO } from '$lib/types';
import type { AchievementCategory } from '$lib/types';
import AchievementCard from '$lib/components/AchievementCard.svelte';
import {
ArrowLeft,
Trophy,
Star,
} from '@manacore/shared-icons';
import { ArrowLeft, Trophy, Star } from '@manacore/shared-icons';
let selectedCategory = $state<AchievementCategory | 'all'>('all');
let showOnlyUnlocked = $state(false);
@ -23,7 +19,10 @@
return list.sort((a, b) => a.sortOrder - b.sortOrder);
});
const categoryEntries = Object.entries(ACHIEVEMENT_CATEGORY_INFO) as [AchievementCategory, { name: string; icon: string }][];
const categoryEntries = Object.entries(ACHIEVEMENT_CATEGORY_INFO) as [
AchievementCategory,
{ name: string; icon: string },
][];
</script>
<div class="min-h-screen">
@ -60,7 +59,9 @@
<div class="mb-8 rounded-xl border border-gray-700 bg-gray-800/50 p-6">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-white">Fortschritt</h2>
<span class="text-2xl font-bold text-yellow-400">{achievementStore.completionPercentage()}%</span>
<span class="text-2xl font-bold text-yellow-400"
>{achievementStore.completionPercentage()}%</span
>
</div>
<div class="h-3 overflow-hidden rounded-full bg-gray-700">
<div
@ -70,7 +71,9 @@
</div>
<div class="mt-3 flex flex-wrap gap-4 text-sm">
{#each Object.entries(RARITY_INFO) as [rarity, info]}
{@const count = achievementStore.achievements.filter((a) => a.rarity === rarity && a.unlocked).length}
{@const count = achievementStore.achievements.filter(
(a) => a.rarity === rarity && a.unlocked
).length}
{@const total = achievementStore.achievements.filter((a) => a.rarity === rarity).length}
<span class="flex items-center gap-1.5 {info.color}">
<Star class="h-3 w-3" />

View file

@ -0,0 +1 @@
export const prerender = true;