mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
02215dfb12
commit
61c23d5e79
45 changed files with 185 additions and 100 deletions
1
apps/calendar/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/calendar/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -42,6 +42,7 @@ 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-stores ./packages/shared-stores
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy chat packages
|
||||
COPY apps/chat/packages ./apps/chat/packages
|
||||
|
|
|
|||
1
apps/chat/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/chat/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/citycorners/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/citycorners/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -42,6 +42,7 @@ 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-stores ./packages/shared-stores
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy clock packages and web
|
||||
COPY apps/clock/packages ./apps/clock/packages
|
||||
|
|
|
|||
1
apps/clock/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/clock/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/contacts/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/contacts/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/context/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/context/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -78,72 +78,72 @@
|
|||
</script>
|
||||
|
||||
<Card class="relative h-full">
|
||||
<!-- Edit Mode Overlay -->
|
||||
{#if dashboardStore.isEditing}
|
||||
<!-- Edit Mode Overlay -->
|
||||
{#if dashboardStore.isEditing}
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex flex-col rounded-xl border-2 border-dashed border-primary/50 bg-background/80"
|
||||
>
|
||||
<!-- Drag Handle -->
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex flex-col rounded-xl border-2 border-dashed border-primary/50 bg-background/80"
|
||||
class="flex cursor-grab items-center justify-center gap-2 border-b border-border py-2 active:cursor-grabbing"
|
||||
>
|
||||
<!-- Drag Handle -->
|
||||
<div
|
||||
class="flex cursor-grab items-center justify-center gap-2 border-b border-border py-2 active:cursor-grabbing"
|
||||
>
|
||||
<svg class="h-5 w-5 text-muted-foreground" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="9" cy="5" r="1.5" />
|
||||
<circle cx="15" cy="5" r="1.5" />
|
||||
<circle cx="9" cy="12" r="1.5" />
|
||||
<circle cx="15" cy="12" r="1.5" />
|
||||
<circle cx="9" cy="19" r="1.5" />
|
||||
<circle cx="15" cy="19" r="1.5" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{meta?.icon} {$_(widget.title)}</span>
|
||||
</div>
|
||||
<svg class="h-5 w-5 text-muted-foreground" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="9" cy="5" r="1.5" />
|
||||
<circle cx="15" cy="5" r="1.5" />
|
||||
<circle cx="9" cy="12" r="1.5" />
|
||||
<circle cx="15" cy="12" r="1.5" />
|
||||
<circle cx="9" cy="19" r="1.5" />
|
||||
<circle cx="15" cy="19" r="1.5" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{meta?.icon} {$_(widget.title)}</span>
|
||||
</div>
|
||||
|
||||
<!-- Size Buttons -->
|
||||
<div class="flex flex-1 items-center justify-center gap-2 p-4">
|
||||
{#each sizes as size}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => handleSizeChange(size)}
|
||||
class="rounded-lg px-3 py-1.5 text-sm font-medium transition-colors {widget.size ===
|
||||
size
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
|
||||
>
|
||||
{sizeLabels[size]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Remove Button -->
|
||||
<div class="flex justify-center border-t border-border py-2">
|
||||
<!-- Size Buttons -->
|
||||
<div class="flex flex-1 items-center justify-center gap-2 p-4">
|
||||
{#each sizes as size}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleRemove}
|
||||
class="flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10"
|
||||
onclick={() => handleSizeChange(size)}
|
||||
class="rounded-lg px-3 py-1.5 text-sm font-medium transition-colors {widget.size ===
|
||||
size
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
{$_('dashboard.remove_widget')}
|
||||
{sizeLabels[size]}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Widget Content -->
|
||||
<div class="min-h-[10rem] p-4" class:opacity-0={dashboardStore.isEditing}>
|
||||
{#if WidgetComponent}
|
||||
<WidgetComponent />
|
||||
{:else}
|
||||
<p class="text-muted-foreground">Unknown widget type: {widget.type}</p>
|
||||
{/if}
|
||||
<!-- Remove Button -->
|
||||
<div class="flex justify-center border-t border-border py-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleRemove}
|
||||
class="flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
{$_('dashboard.remove_widget')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Widget Content -->
|
||||
<div class="min-h-[10rem] p-4" class:opacity-0={dashboardStore.isEditing}>
|
||||
{#if WidgetComponent}
|
||||
<WidgetComponent />
|
||||
{:else}
|
||||
<p class="text-muted-foreground">Unknown widget type: {widget.type}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
1
apps/manacore/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/manacore/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/manadeck/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/manadeck/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/matrix/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/matrix/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -43,6 +43,7 @@ COPY packages/shared-ui ./packages/shared-ui
|
|||
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-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy mukke shared package
|
||||
COPY apps/mukke/packages ./apps/mukke/packages
|
||||
|
|
|
|||
1
apps/mukke/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/mukke/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/nutriphi/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/nutriphi/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -49,6 +49,7 @@ 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-stores ./packages/shared-stores
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy photos shared package
|
||||
COPY apps/photos/packages/shared ./apps/photos/packages/shared
|
||||
|
|
|
|||
1
apps/photos/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/photos/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -48,6 +48,7 @@ COPY packages/shared-vite-config ./packages/shared-vite-config
|
|||
COPY packages/shared-api-client ./packages/shared-api-client
|
||||
COPY packages/shared-stores ./packages/shared-stores
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy picture packages
|
||||
COPY apps/picture/packages/shared ./apps/picture/packages/shared
|
||||
|
|
|
|||
1
apps/picture/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/picture/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/planta/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/planta/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -42,6 +42,7 @@ COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
|||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-error-tracking ./packages/shared-error-tracking
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy presi web
|
||||
COPY apps/presi/apps/web ./apps/presi/apps/web
|
||||
|
|
|
|||
1
apps/presi/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/presi/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
1
apps/questions/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/questions/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
1
apps/skilltree/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/skilltree/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -47,6 +47,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-pwa ./packages/shared-pwa
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy storage web
|
||||
COPY apps/storage/apps/web ./apps/storage/apps/web
|
||||
|
|
|
|||
1
apps/storage/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/storage/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
34
apps/todo/apps/web/scripts/generate-icons.mjs
Normal file
34
apps/todo/apps/web/scripts/generate-icons.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env node
|
||||
import { readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const staticDir = join(__dirname, '..', 'static');
|
||||
|
||||
const sizes = [
|
||||
{ name: 'favicon.png', size: 32 },
|
||||
{ name: 'pwa-192x192.png', size: 192 },
|
||||
{ name: 'pwa-512x512.png', size: 512 },
|
||||
{ name: 'apple-touch-icon.png', size: 180 },
|
||||
];
|
||||
|
||||
async function generateIcons() {
|
||||
try {
|
||||
const sharp = (await import('sharp')).default;
|
||||
const svgPath = join(staticDir, 'icons', 'icon.svg');
|
||||
const svgBuffer = readFileSync(svgPath);
|
||||
|
||||
for (const { name, size } of sizes) {
|
||||
const outputPath = join(staticDir, name);
|
||||
await sharp(svgBuffer).resize(size, size).png().toFile(outputPath);
|
||||
console.log(`Generated: ${name} (${size}x${size})`);
|
||||
}
|
||||
console.log('\nAll icons generated!');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateIcons();
|
||||
|
|
@ -4,16 +4,21 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg" />
|
||||
|
||||
<!-- PWA meta tags -->
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="theme-color" content="#8b5cf6" />
|
||||
<meta name="application-name" content="Todo" />
|
||||
<meta name="description" content="Aufgaben und Projekte verwalten" />
|
||||
|
||||
<!-- PWA/iOS Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Todo" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon.svg" />
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/icons/icon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon.png" />
|
||||
|
||||
<title>Todo</title>
|
||||
%sveltekit.head%
|
||||
|
|
|
|||
1
apps/todo/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/todo/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
BIN
apps/todo/apps/web/static/apple-touch-icon.png
Normal file
BIN
apps/todo/apps/web/static/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 1.1 KiB |
BIN
apps/todo/apps/web/static/pwa-192x192.png
Normal file
BIN
apps/todo/apps/web/static/pwa-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
apps/todo/apps/web/static/pwa-512x512.png
Normal file
BIN
apps/todo/apps/web/static/pwa-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -43,6 +43,7 @@ COPY packages/shared-ui ./packages/shared-ui
|
|||
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-app-onboarding ./packages/shared-app-onboarding
|
||||
|
||||
# Copy zitare content package
|
||||
COPY apps/zitare/packages/content ./apps/zitare/packages/content
|
||||
|
|
|
|||
1
apps/zitare/apps/web/src/routes/offline/+page.ts
Normal file
1
apps/zitare/apps/web/src/routes/offline/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
Loading…
Add table
Add a link
Reference in a new issue