diff --git a/src/lib/components/ProfileButton.svelte b/src/lib/components/ProfileButton.svelte index 50ba784..afd3639 100644 --- a/src/lib/components/ProfileButton.svelte +++ b/src/lib/components/ProfileButton.svelte @@ -65,7 +65,7 @@ diff --git a/src/lib/index.ts b/src/lib/index.ts index 856f2b6..cb04cd1 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,3 @@ // place files you want to import through the `$lib` alias in this folder. + +export { formatBounty } from './utils'; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..d0b4bd8 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,13 @@ +export function formatBounty(bounty: number): string { + if (bounty >= 1_000_000_000) { + const billions = bounty / 1_000_000_000; + return `${billions}B`; + } else if (bounty >= 1_000_000) { + const millions = bounty / 1_000_000; + return `${millions}M`; + } else if (bounty >= 1_000) { + const thousands = bounty / 1_000; + return `${thousands}K`; + } + return bounty.toString(); +} diff --git a/src/routes/(admin)/admin/+layout.server.ts b/src/routes/(admin)/admin/+layout.server.ts new file mode 100644 index 0000000..84b4f88 --- /dev/null +++ b/src/routes/(admin)/admin/+layout.server.ts @@ -0,0 +1,12 @@ +import { redirect } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + if (!locals.user) { + redirect(302, '/login'); + } + + if (!locals.user.isAdmin) { + redirect(302, '/'); + } +}; diff --git a/src/routes/(admin)/admin/+layout.svelte b/src/routes/(admin)/admin/+layout.svelte new file mode 100644 index 0000000..7014950 --- /dev/null +++ b/src/routes/(admin)/admin/+layout.svelte @@ -0,0 +1,57 @@ + + +
+ + + + +
+
+

Admin Dashboard

+ +
+
+ {@render children()} +
+
+
diff --git a/src/routes/(admin)/admin/+page.server.ts b/src/routes/(admin)/admin/+page.server.ts new file mode 100644 index 0000000..521e3e4 --- /dev/null +++ b/src/routes/(admin)/admin/+page.server.ts @@ -0,0 +1,37 @@ +import { db } from '$lib/server/db'; +import { character, devilFruit, arc, user, config, characterHistory } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const [characters, devilFruits, arcs, users, configEntries, history] = await Promise.all([ + db.select().from(character), + db.select().from(devilFruit), + db.select().from(arc), + db.select().from(user), + db.select().from(config), + db.select().from(characterHistory) + ]); + + // Get daily character ID from config + const dailyCharIdEntry = configEntries.find((c) => c.key === 'dailyCharacterId'); + const dailyCharId = dailyCharIdEntry?.value; + + // Count how many times today's daily character was won/found + const today = new Date().toISOString().split('T')[0]; + const dailyCharacterWins = dailyCharId + ? history.filter((h) => h.characterId === dailyCharId && h.date === today && h.won === 1).length + : 0; + + return { + stats: { + totalCharacters: characters.length, + charactersInDaily: characters.filter((c) => c.isInDailyMode).length, + totalDevilFruits: devilFruits.length, + totalArcs: arcs.length, + totalUsers: users.length, + adminUsers: users.filter((u) => u.isAdmin).length, + dailyCharacterWins + } + }; +}; diff --git a/src/routes/(admin)/admin/+page.svelte b/src/routes/(admin)/admin/+page.svelte new file mode 100644 index 0000000..10dfc22 --- /dev/null +++ b/src/routes/(admin)/admin/+page.svelte @@ -0,0 +1,121 @@ + + + + Admin Dashboard - OnePieceDle + + +
+ +
+

Welcome Back!

+

+ {#if data.stats.dailyCharacterWins > 0} + {data.stats.dailyCharacterWins} + {data.stats.dailyCharacterWins === 1 ? 'person has' : 'people have'} found today's daily character! + {:else} + No one has found today's daily character yet. + {/if} +

+
+ + +
+ {#each statCards as card} +
+
+
+

{card.label}

+

{card.value}

+
+
{card.icon}
+
+
+ {/each} +
+ + +
+

Quick Actions

+ +
+
diff --git a/src/routes/(admin)/admin/arcs/+page.server.ts b/src/routes/(admin)/admin/arcs/+page.server.ts new file mode 100644 index 0000000..a578f21 --- /dev/null +++ b/src/routes/(admin)/admin/arcs/+page.server.ts @@ -0,0 +1,69 @@ +import { db } from '$lib/server/db'; +import { arc } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async () => { + const arcs = await db.select().from(arc).orderBy(arc.name); + + return { + arcs + }; +}; + +export const actions: Actions = { + update: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Arc ID is required' }); + } + + try { + const updates: Record = {}; + formData.forEach((value, key) => { + if (key !== 'id') { + if (key === 'startChapter' || key === 'endChapter') { + updates[key] = value ? parseInt(value as string) : null; + } else { + updates[key] = value || null; + } + } + }); + + await db.update(arc).set(updates).where(eq(arc.id, id)); + return { success: true }; + } catch (error) { + console.error('Arc update error:', error); + return fail(500, { error: 'Failed to update arc' }); + } + }, + + delete: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Arc ID is required' }); + } + + try { + await db.delete(arc).where(eq(arc.id, id)); + return { success: true }; + } catch (error) { + console.error('Arc delete error:', error); + return fail(500, { error: 'Failed to delete arc' }); + } + } +}; + diff --git a/src/routes/(admin)/admin/arcs/+page.svelte b/src/routes/(admin)/admin/arcs/+page.svelte new file mode 100644 index 0000000..e5d5a48 --- /dev/null +++ b/src/routes/(admin)/admin/arcs/+page.svelte @@ -0,0 +1,246 @@ + + + + Arcs - Admin - OnePieceDle + + +
+ +
+

Arc Management

+ +
+ + +
+ +
+ + +
+
+ + + + + + + + + + + {#each filteredArcs as arc} + + + + + + + {/each} + +
NameStart ChapterEnd ChapterActions
{arc.name}{arc.startChapter}{arc.endChapter || '-'} +
+ + +
+
+
+ {#if filteredArcs.length === 0} +
+

No arcs found

+
+ {/if} + + + {#if isEditModalOpen} +
+
+

Edit Arc

+
{ + isSaving = true; + saveMessage = null; + return async ({ result }) => { + isSaving = false; + if (result.type === 'success') { + saveMessage = { type: 'success', message: 'Arc updated successfully' }; + setTimeout(() => { + closeModal(); + window.location.reload(); + }, 500); + } else if (result.type === 'failure') { + saveMessage = { type: 'error', message: (result.data as any)?.error || 'Failed to update arc' }; + } + }; + }} + > + +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ {#if saveMessage} +
+ {saveMessage.message} +
+ {/if} +
+ + +
+
+
+
+ {/if} +
diff --git a/src/routes/(admin)/admin/characters/+page.server.ts b/src/routes/(admin)/admin/characters/+page.server.ts new file mode 100644 index 0000000..734b4ff --- /dev/null +++ b/src/routes/(admin)/admin/characters/+page.server.ts @@ -0,0 +1,171 @@ +import { db } from '$lib/server/db'; +import { character, devilFruit, arc, characterOverride } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async () => { + const [charactersData, devilFruits, arcs, overrides] = await Promise.all([ + db + .select({ + id: character.id, + name: character.name, + gender: character.gender, + age: character.age, + affiliations: character.affiliations, + devilFruitId: character.devilFruitId, + hakiObservation: character.hakiObservation, + hakiArmament: character.hakiArmament, + hakiConqueror: character.hakiConqueror, + bounty: character.bounty, + height: character.height, + origin: character.origin, + firstAppearance: character.firstAppearance, + pictureUrl: character.pictureUrl, + epithets: character.epithets, + status: character.status, + url: character.url, + arcId: character.arcId, + isInDailyMode: character.isInDailyMode, + arcName: arc.name, + devilFruitName: devilFruit.name, + devilFruitType: devilFruit.type + }) + .from(character) + .leftJoin(arc, eq(character.arcId, arc.id)) + .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) + .orderBy(character.name), + db.select().from(devilFruit).orderBy(devilFruit.name), + db.select().from(arc).orderBy(arc.name), + db.select().from(characterOverride) + ]); + + // Create a map of overrides by characterId for easy lookup + const overridesMap = new Map(overrides.map((o) => [o.characterId, o])); + + // Merge character data with overrides + const charactersWithOverrides = charactersData.map((char) => { + const override = overridesMap.get(char.id); + + // Build displayValues by only applying non-null override fields + const displayValues = { ...char } as any; + if (override) { + Object.keys(override).forEach((key) => { + if (override[key as keyof typeof override] !== null && key !== 'characterId') { + displayValues[key as keyof typeof displayValues] = override[key as keyof typeof override]; + } + }); + } + + return { + ...char, + override, + displayValues + }; + }); + + return { + characters: charactersWithOverrides, + devilFruits, + arcs + }; +}; + +export const actions: Actions = { + update: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Character ID is required' }); + } + + try { + const updates: Record = { + // Initialize boolean fields to false (they'll be set to true if present in formData) + hakiObservation: false, + hakiArmament: false, + hakiConqueror: false + }; + + formData.forEach((value, key) => { + if (key !== 'id') { + // Handle checkboxes (haki fields) + if (key === 'hakiObservation' || key === 'hakiArmament' || key === 'hakiConqueror') { + updates[key] = value === 'on'; + } + // Handle integers (age, bounty, height, devilFruitId, arcId) + else if (key === 'age' || key === 'bounty' || key === 'height' || key === 'devilFruitId' || key === 'arcId') { + const strValue = value as string; + updates[key] = strValue && strValue !== '' ? parseInt(strValue) : null; + } + // Handle strings (name, gender, status, origin, affiliations, epithets, pictureUrl, url, firstAppearance) + else { + updates[key] = value || null; + } + } + }); + + // Update or insert into characterOverride table + await db + .insert(characterOverride) + .values({ characterId: id, ...updates }) + .onConflictDoUpdate({ target: characterOverride.characterId, set: updates }); + + return { success: true }; + } catch (error) { + console.error('Character update error:', error); + return fail(500, { error: 'Failed to update character' }); + } + }, + + delete: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Character ID is required' }); + } + + try { + await db.delete(character).where(eq(character.id, id)); + return { success: true }; + } catch (error) { + console.error('Character delete error:', error); + return fail(500, { error: 'Failed to delete character' }); + } + }, + + toggleDailyMode: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + const isInDailyMode = formData.get('isInDailyMode') === 'true'; + + if (!id) { + return fail(400, { error: 'Character ID is required' }); + } + + try { + await db.update(character) + .set({ isInDailyMode }) + .where(eq(character.id, id)); + + return { success: true }; + } catch (error) { + console.error('Toggle daily mode error:', error); + return fail(500, { error: 'Failed to toggle daily mode' }); + } + } +}; diff --git a/src/routes/(admin)/admin/characters/+page.svelte b/src/routes/(admin)/admin/characters/+page.svelte new file mode 100644 index 0000000..9026238 --- /dev/null +++ b/src/routes/(admin)/admin/characters/+page.svelte @@ -0,0 +1,828 @@ + + + + Characters - Admin - OnePieceDle + + +
+ +
+

Character Management

+ +
+ + +
+ + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + {#each filteredCharacters as char} + + + + + + + + + + + + + + + + + + + + + + + + + + + {/each} + +
CharacterStatusGenderAffiliationsFruitHakiBountyHeightOriginArcDaily ModeActions
+
+ {#if getFandomUrl(char.displayValues.url)} + + {#if char.displayValues.pictureUrl} + {char.displayValues.name} + {:else} +
+ {char.displayValues.name?.charAt(0).toUpperCase() || '?'} +
+ {/if} +
+ {:else} + {#if char.displayValues.pictureUrl} + {char.displayValues.name} + {:else} +
+ {char.displayValues.name?.charAt(0).toUpperCase() || '?'} +
+ {/if} + {/if} +
+ {#if getFandomUrl(char.displayValues.url)} + + {char.displayValues.name} + + {:else} + {char.displayValues.name} + {/if} + {#if char.displayValues.epithets} + + {typeof char.displayValues.epithets === 'string' + ? (char.displayValues.epithets.includes('[') ? JSON.parse(char.displayValues.epithets).join(', ') : char.displayValues.epithets) + : char.displayValues.epithets.join(', ')} + + {/if} +
+
+
{char.displayValues.status || '-'}{char.displayValues.gender || '-'} + {#if char.displayValues.affiliations} + {@const parsedAffiliations = typeof char.displayValues.affiliations === 'string' + ? (char.displayValues.affiliations.includes('[') ? JSON.parse(char.displayValues.affiliations) : char.displayValues.affiliations.split(',').map((a: string) => a.trim())) + : char.displayValues.affiliations} + {#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0} + {parsedAffiliations[0]} + {:else} + {parsedAffiliations} + {/if} + {:else} + - + {/if} + {char.displayValues.devilFruitName || '-'} +
+ {#if char.displayValues.hakiObservation}👁ïļ{/if} + {#if char.displayValues.hakiArmament}ðŸĶū{/if} + {#if char.displayValues.hakiConqueror}👑{/if} + {#if !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror} + - + {/if} +
+
+ {#if char.displayValues.bounty != null} + {formatBounty(char.displayValues.bounty)} āļŋ + {:else} + - + {/if} + + {#if char.displayValues.height} + {char.displayValues.height} m + {:else} + - + {/if} + {char.displayValues.origin || '-'}{char.displayValues.arcName || '-'} +
{ + return async ({ result, update }) => { + if (result.type === 'success') { + await update(); + showDailyModeToast('success', 'Daily mode updated successfully!'); + } else if (result.type === 'failure') { + showDailyModeToast('error', (result.data as any)?.error || 'Failed to update daily mode'); + } else { + showDailyModeToast('error', 'Failed to update daily mode'); + } + }; + }} + > + + + +
+
+
+ + +
+
+
+
+ + {#if filteredCharacters.length === 0} +
+

No characters found

+
+ {/if} + + {#if dailyModeToast} +
+
+ {dailyModeToast.text} +
+
+ {/if} + + + {#if isEditModalOpen} +
+
+

Edit Character

+
{ + isSaving = true; + return async ({ result }) => { + isSaving = false; + if (result.type === 'success') { + saveMessage = { type: 'success', text: 'Character saved successfully!' }; + setTimeout(() => { + location.reload(); + }, 1000); + } else if (result.type === 'failure') { + saveMessage = { type: 'error', text: (result.data as any)?.error || 'Failed to save character' }; + } + setTimeout(() => { + saveMessage = null; + }, 3000); + }; + }} + > + + + +
+

Basic Information

+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+

Physical Attributes

+ +
+
+ + +
+
+ + +
+
+
+ + +
+

Location & Affiliations

+ +
+ + +
+ +
+ + +
+ +
+ + + {#if selectedChar?.arcName} +

Original: {selectedChar.arcName}

+ {/if} +
+
+ + +
+

Powers

+ +
+ + + {#if selectedChar?.devilFruitName} +

Original: {selectedChar.devilFruitName}

+ {/if} +
+ +
+

Haki

+
+ + + +
+
+
+ + +
+

Timeline

+ +
+ + +
+
+ + +
+

Media & Details

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ {#if saveMessage} +
+ {saveMessage.text} +
+ {/if} +
+
+ {/if} +
diff --git a/src/routes/(admin)/admin/config/+page.server.ts b/src/routes/(admin)/admin/config/+page.server.ts new file mode 100644 index 0000000..58f3a0c --- /dev/null +++ b/src/routes/(admin)/admin/config/+page.server.ts @@ -0,0 +1,62 @@ +import { db } from '$lib/server/db'; +import { config } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async () => { + const configEntries = await db.select().from(config); + + return { + config: configEntries + }; +}; + +export const actions: Actions = { + update: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const key = formData.get('key') as string; + const value = formData.get('value') as string; + + if (!key) { + return fail(400, { error: 'Config key is required' }); + } + + try { + await db + .insert(config) + .values({ key, value }) + .onConflictDoUpdate({ target: config.key, set: { value } }); + + return { success: true }; + } catch (error) { + console.error('Config update error:', error); + return fail(500, { error: 'Failed to update configuration' }); + } + }, + + delete: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const key = formData.get('key') as string; + + if (!key) { + return fail(400, { error: 'Config key is required' }); + } + + try { + await db.delete(config).where(eq(config.key, key)); + return { success: true }; + } catch (error) { + console.error('Config delete error:', error); + return fail(500, { error: 'Failed to delete configuration' }); + } + } +}; \ No newline at end of file diff --git a/src/routes/(admin)/admin/config/+page.svelte b/src/routes/(admin)/admin/config/+page.svelte new file mode 100644 index 0000000..80b64de --- /dev/null +++ b/src/routes/(admin)/admin/config/+page.svelte @@ -0,0 +1,266 @@ + + + + Settings - Admin - OnePieceDle + + +
+ +

Configuration

+ + +
+

Add New Configuration

+
+ + + +
+
+ + +
+
+ + + + + + + + + + {#each configItems as item} + {#if editingKey === item.key} + + + + + + {:else} + + + + + + {/if} + {/each} + +
KeyValueActions
{item.key} + + +
{ + isSaving = true; + return async ({ result }) => { + isSaving = false; + if (result.type === 'success') { + const idx = configItems.findIndex((i) => i.key === item.key); + if (idx !== -1) { + configItems[idx].value = editingValue; + } + editingKey = null; + saveMessage = { type: 'success', text: 'Config updated' }; + } else if (result.type === 'failure') { + saveMessage = { type: 'error', text: (result.data?.error as string) || 'Failed to update' }; + } else { + saveMessage = { type: 'error', text: 'Failed to update' }; + } + setTimeout(() => { + saveMessage = null; + }, 3000); + }; + }} + > + + +
+ + +
+
+
{item.key} + {item.value} + +
+ + +
+
+ + {#if configItems.length === 0} +
+

No configuration entries yet

+
+ {/if} + + + {#if saveMessage} +
+ {saveMessage.text} +
+ {/if} +
diff --git a/src/routes/(admin)/admin/devil-fruits/+page.server.ts b/src/routes/(admin)/admin/devil-fruits/+page.server.ts new file mode 100644 index 0000000..99e1c1b --- /dev/null +++ b/src/routes/(admin)/admin/devil-fruits/+page.server.ts @@ -0,0 +1,65 @@ +import { db } from '$lib/server/db'; +import { devilFruit } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async () => { + const devilFruits = await db.select().from(devilFruit).orderBy(devilFruit.name); + + return { + devilFruits + }; +}; + +export const actions: Actions = { + update: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Devil Fruit ID is required' }); + } + + try { + const updates: Record = {}; + formData.forEach((value, key) => { + if (key !== 'id') { + updates[key] = value || null; + } + }); + + await db.update(devilFruit).set(updates).where(eq(devilFruit.id, id)); + return { success: true }; + } catch (error) { + console.error('Devil Fruit update error:', error); + return fail(500, { error: 'Failed to update devil fruit' }); + } + }, + + delete: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Devil Fruit ID is required' }); + } + + try { + await db.delete(devilFruit).where(eq(devilFruit.id, id)); + return { success: true }; + } catch (error) { + console.error('Devil Fruit delete error:', error); + return fail(500, { error: 'Failed to delete devil fruit' }); + } + } +}; + diff --git a/src/routes/(admin)/admin/devil-fruits/+page.svelte b/src/routes/(admin)/admin/devil-fruits/+page.svelte new file mode 100644 index 0000000..4675149 --- /dev/null +++ b/src/routes/(admin)/admin/devil-fruits/+page.svelte @@ -0,0 +1,284 @@ + + + + Devil Fruits - Admin - OnePieceDle + + +
+ +
+

Devil Fruit Management

+ +
+ + +
+ + +
+ + +
+
+ + + + + + + + + + {#each filteredFruits as fruit} + + + + + + {/each} + +
NameTypeActions
{fruit.name} + + {fruit.type || 'Unknown'} + + +
+ + +
+
+
+ {#if filteredFruits.length === 0} +
+

No devil fruits found

+
+ {/if} + + + {#if isEditModalOpen} +
+
+

Edit Devil Fruit

+
{ + isSaving = true; + return async ({ result }) => { + isSaving = false; + if (result.type === 'success') { + saveMessage = { type: 'success', text: 'Devil Fruit saved successfully!' }; + setTimeout(() => { + location.reload(); + }, 1000); + } else if (result.type === 'failure') { + saveMessage = { type: 'error', text: (result.data as any)?.error || 'Failed to save devil fruit' }; + } + setTimeout(() => { + saveMessage = null; + }, 3000); + }; + }} + > + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ {#if saveMessage} +
+ {saveMessage.text} +
+ {/if} +
+
+ {/if} +
diff --git a/src/routes/(admin)/admin/users/+page.server.ts b/src/routes/(admin)/admin/users/+page.server.ts new file mode 100644 index 0000000..04da45b --- /dev/null +++ b/src/routes/(admin)/admin/users/+page.server.ts @@ -0,0 +1,67 @@ +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async () => { + const users = await db.select().from(user).orderBy(user.createdAt); + + return { + users: users.map((u) => ({ + ...u, + createdAt: new Date(u.createdAt).toLocaleDateString(), + updatedAt: new Date(u.updatedAt).toLocaleDateString() + })) + }; +}; + +export const actions: Actions = { + update: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'User ID is required' }); + } + + try { + const updates: Record = { + name: formData.get('name') as string, + email: formData.get('email') as string, + isAdmin: formData.has('isAdmin'), + emailVerified: formData.has('emailVerified') + }; + + await db.update(user).set(updates).where(eq(user.id, id)); + return { success: true }; + } catch (error) { + console.error('User update error:', error); + return fail(500, { error: 'Failed to update user' }); + } + }, + + delete: async ({ request, locals }) => { + if (!locals.user?.isAdmin) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'User ID is required' }); + } + + try { + await db.delete(user).where(eq(user.id, id)); + return { success: true }; + } catch (error) { + console.error('User delete error:', error); + return fail(500, { error: 'Failed to delete user' }); + } + }}; \ No newline at end of file diff --git a/src/routes/(admin)/admin/users/+page.svelte b/src/routes/(admin)/admin/users/+page.svelte new file mode 100644 index 0000000..9b7ea99 --- /dev/null +++ b/src/routes/(admin)/admin/users/+page.svelte @@ -0,0 +1,278 @@ + + + + Users - Admin - OnePieceDle + + +
+ +
+

User Management

+
+ + +
+ + +
+ + +
+
+ + + + + + + + + + + + + {#each filteredUsers as usr} + + + + + + + + + {/each} + +
NameEmailRoleVerifiedJoinedActions
{usr.name}{usr.email} + {#if usr.isAdmin} + + Admin + + {:else} + + User + + {/if} + + {#if usr.emailVerified} + + ✓ + + {:else} + + ✗ + + {/if} + {usr.createdAt} +
+ + +
+
+
+
+ + {#if filteredUsers.length === 0} +
+

No users found

+
+ {/if} + + + {#if isEditModalOpen} +
+
+

Edit User

+
{ + isSaving = true; + saveMessage = null; + return async ({ result }) => { + isSaving = false; + if (result.type === 'success') { + saveMessage = { type: 'success', message: 'User updated successfully' }; + setTimeout(() => { + closeModal(); + window.location.reload(); + }, 500); + } else if (result.type === 'failure') { + saveMessage = { type: 'error', message: String(result.data?.error) || 'Failed to update user' }; + } + }; + }} + > + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {#if saveMessage} +
+ {saveMessage.message} +
+ {/if} +
+ + +
+
+
+
+ {/if} +
diff --git a/src/routes/(game)/+layout.svelte b/src/routes/(game)/+layout.svelte new file mode 100644 index 0000000..f9b6fb9 --- /dev/null +++ b/src/routes/(game)/+layout.svelte @@ -0,0 +1,19 @@ + + +
+
+ +
+
+ {@render children()} +
+
diff --git a/src/routes/+page.server.ts b/src/routes/(game)/+page.server.ts similarity index 100% rename from src/routes/+page.server.ts rename to src/routes/(game)/+page.server.ts diff --git a/src/routes/+page.svelte b/src/routes/(game)/+page.svelte similarity index 100% rename from src/routes/+page.svelte rename to src/routes/(game)/+page.svelte diff --git a/src/routes/daily/+page.server.ts b/src/routes/(game)/daily/+page.server.ts similarity index 100% rename from src/routes/daily/+page.server.ts rename to src/routes/(game)/daily/+page.server.ts diff --git a/src/routes/daily/+page.svelte b/src/routes/(game)/daily/+page.svelte similarity index 98% rename from src/routes/daily/+page.svelte rename to src/routes/(game)/daily/+page.svelte index ce3f79d..dc981f3 100644 --- a/src/routes/daily/+page.svelte +++ b/src/routes/(game)/daily/+page.svelte @@ -1,5 +1,6 @@ diff --git a/src/routes/daily/+server.ts b/src/routes/(game)/daily/+server.ts similarity index 100% rename from src/routes/daily/+server.ts rename to src/routes/(game)/daily/+server.ts diff --git a/src/routes/login/+page.server.ts b/src/routes/(game)/login/+page.server.ts similarity index 100% rename from src/routes/login/+page.server.ts rename to src/routes/(game)/login/+page.server.ts diff --git a/src/routes/login/+page.svelte b/src/routes/(game)/login/+page.svelte similarity index 100% rename from src/routes/login/+page.svelte rename to src/routes/(game)/login/+page.svelte diff --git a/src/routes/profile/+page.server.ts b/src/routes/(game)/profile/+page.server.ts similarity index 100% rename from src/routes/profile/+page.server.ts rename to src/routes/(game)/profile/+page.server.ts diff --git a/src/routes/profile/+page.svelte b/src/routes/(game)/profile/+page.svelte similarity index 100% rename from src/routes/profile/+page.svelte rename to src/routes/(game)/profile/+page.svelte diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8030a96..341c8d8 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,23 +1,11 @@ -
-
- -
-
- {@render children()} -
-
+ +{@render children()} \ No newline at end of file