-
Notifications
You must be signed in to change notification settings - Fork 1
Backgrounds in User Shop #426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stage
Are you sure you want to change the base?
Changes from 8 commits
3c7a871
534fb52
170cf21
1121f9e
5dd1386
dd0ba13
1e3c513
85c3841
f69e66c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,78 @@ | ||
| <script setup lang="ts"> | ||
| definePageMeta({ ssr: false }) | ||
|
|
||
| const settings = reactive({ theme: 'light', dyslexiaFont: false, language: 'en', fontSize: 1 }) | ||
| const stats = reactive({ xp: 1250, booksRead: 14, streak: 5, tickets: 3 }) | ||
| const { student, settings, saveSettings, restoreStudent, updateExp } = useCurrentStudent() | ||
| const { tickets, loadProgress } = useCurrentStudentProgress() | ||
|
|
||
| type ShopItemUi = { | ||
| id: number | ||
| type: string | ||
| name: string | ||
| cost: number | ||
| owned: boolean | ||
| class: string | ||
| previewBg?: string | ||
| previewGrad?: string | ||
| } | ||
|
|
||
| const stats = computed(() => ({ | ||
| xp: student.value ? student.value.exp : 0, | ||
| tickets: tickets.value ? tickets.value : 0, | ||
| })) | ||
|
|
||
| async function loadShopItems() { | ||
| if (!student.value?.id) return | ||
|
|
||
| const rawItems = await $fetch<any[]>('/api/shop', { | ||
| query: { studentId: student.value.id }, | ||
| }) | ||
|
|
||
| // Fallback mapping keeps current visual cards while moving source of truth to DB. | ||
| const styleMap: Record<string, { class: string; previewBg: string; previewGrad: string }> = { | ||
| 'Light Bloom': { class: 'light', previewBg: '#f5ede3', previewGrad: 'radial-gradient(at 0% 0%, hsla(25,95%,75%,0.3) 0px, transparent 50%)' }, | ||
| 'Galaxy Night': { class: 'dark', previewBg: '#1f3b7c', previewGrad: 'radial-gradient(at 0% 0%, hsla(250,20%,20%,0.5) 0px, transparent 50%)' }, | ||
| 'Old Parchment': { class: 'sepia', previewBg: '#f4ecd8', previewGrad: 'none' }, | ||
| Sunset: { class: 'sunset', previewBg: '#fff5f5', previewGrad: 'radial-gradient(at 0% 0%, hsla(10,90%,75%,0.25) 0px, transparent 50%)' }, | ||
| Ocean: { class: 'ocean', previewBg: '#f0f9ff', previewGrad: 'radial-gradient(at 0% 0%, hsla(200,90%,75%,0.25) 0px, transparent 50%)' }, | ||
| Forest: { class: 'forest', previewBg: '#f0fdf4', previewGrad: 'radial-gradient(at 0% 0%, hsla(140,80%,70%,0.25) 0px, transparent 50%)' }, | ||
| Candy: { class: 'candy', previewBg: '#fdf2f8', previewGrad: 'radial-gradient(at 0% 0%, hsla(330,90%,85%,0.35) 0px, transparent 50%)' }, | ||
| Fire: { class: 'fire', previewBg: '#fff7ed', previewGrad: 'radial-gradient(at 30% 40%, hsla(20,95%,65%,0.3) 0px, transparent 50%)' }, | ||
| Ice: { class: 'ice', previewBg: '#f0f9ff', previewGrad: 'radial-gradient(at 0% 0%, hsla(200,100%,95%,0.4) 0px, transparent 50%)' }, | ||
| } | ||
|
|
||
| shopItems.value = rawItems.map((item) => { | ||
| const mapped = styleMap[item.name] || { | ||
| class: item.type === 'theme' ? 'light' : '', | ||
| previewBg: '#f5ede3', | ||
| previewGrad: 'none', | ||
| } | ||
|
|
||
| return { | ||
| id: item.id, | ||
| type: item.type, | ||
| name: item.name, | ||
| cost: item.cost, | ||
| owned: Boolean(item.owned) || item.cost === 0, | ||
| class: mapped.class, | ||
| previewBg: mapped.previewBg, | ||
| previewGrad: mapped.previewGrad, | ||
| } as ShopItemUi | ||
| }) | ||
| } | ||
|
|
||
| onMounted(async () => { | ||
| if (!student.value) { | ||
| await restoreStudent() | ||
| } | ||
| if (student.value) { | ||
| await loadProgress() | ||
| await loadShopItems() | ||
| } | ||
| }) | ||
|
|
||
| const themeClass = computed(() => { | ||
| const t = settings.theme !== 'light' ? `theme-${settings.theme}` : '' | ||
| const d = settings.dyslexiaFont ? 'dyslexia-font' : '' | ||
| const t = settings.value.theme !== 'light' ? `theme-${settings.value.theme}` : '' | ||
| const d = settings.value.dyslexiaFont ? 'dyslexia-font' : '' | ||
| return `reader-app ${t} ${d}`.trim() | ||
| }) | ||
|
|
||
|
|
@@ -32,42 +98,44 @@ function triggerTicketClick() { | |
| // | ||
|
|
||
| // ── Shop items — themes ── | ||
| const shopItems = ref([ | ||
| { id: 1, type:'theme', name:'Light Bloom', cost:0, class:'light', owned:true, previewBg:'#f5ede3', previewGrad:'radial-gradient(at 0% 0%, hsla(25,95%,75%,0.3) 0px, transparent 50%)' }, | ||
| { id: 2, type:'theme', name:'Galaxy Night', cost:500, class:'blue', owned:false, previewBg:'#1f3b7c', previewGrad:'radial-gradient(at 0% 0%, hsla(250,20%,20%,0.5) 0px, transparent 50%)' }, | ||
| { id: 3, type:'theme', name:'Old Parchment', cost:300, class:'sepia', owned:false, previewBg:'#f4ecd8', previewGrad:'none' }, | ||
| { id: 10, type:'theme', name:'Sunset', cost:100, class:'sunset', owned:false, previewBg:'#fff5f5', previewGrad:'radial-gradient(at 0% 0%, hsla(10,90%,75%,0.25) 0px, transparent 50%)' }, | ||
| { id: 11, type:'theme', name:'Ocean', cost:100, class:'ocean', owned:false, previewBg:'#f0f9ff', previewGrad:'radial-gradient(at 0% 0%, hsla(200,90%,75%,0.25) 0px, transparent 50%)' }, | ||
| { id: 12, type:'theme', name:'Forest', cost:150, class:'forest', owned:false, previewBg:'#f0fdf4', previewGrad:'radial-gradient(at 0% 0%, hsla(140,80%,70%,0.25) 0px, transparent 50%)' }, | ||
| { id: 13, type:'theme', name:'Candy', cost:150, class:'candy', owned:false, previewBg:'#fdf2f8', previewGrad:'radial-gradient(at 0% 0%, hsla(330,90%,85%,0.35) 0px, transparent 50%)' }, | ||
| { id: 14, type:'theme', name:'Fire', cost:150, class:'fire', owned:false, previewBg:'#fff7ed', previewGrad:'radial-gradient(at 30% 40%, hsla(20,95%,65%,0.3) 0px, transparent 50%)' }, | ||
| { id: 15, type:'theme', name:'Ice', cost:150, class:'ice', owned:false, previewBg:'#f0f9ff', previewGrad:'radial-gradient(at 0% 0%, hsla(200,100%,95%,0.4) 0px, transparent 50%)' }, | ||
| { id: 4, type:'animation', name:'Twinkling Stars', cost:200, owned:false }, | ||
| { id: 5, type:'animation', name:'Confetti Rain', cost:1000, owned:false }, | ||
| { id: 6, type:'animation', name:'Magic Sparkles', cost:400, owned:false }, | ||
| { id: 7, type:'animation', name:'Fireflies', cost:400, owned:false }, | ||
| { id: 8, type:'animation', name:'Fluttering Butterflies', cost:500, owned:false }, | ||
| { id: 9, type:'animation', name:'Falling Leaves', cost:800, owned:false }, | ||
| ]) | ||
| const shopItems = ref<ShopItemUi[]>([]) | ||
|
|
||
| const activeAnimations = ref<string[]>([]) | ||
| const showShopCelebration = ref('') | ||
|
|
||
| function buyItem(item: any) { | ||
| if (stats.xp >= item.cost) { | ||
| stats.xp -= item.cost | ||
| item.owned = true | ||
| showShopCelebration.value = item.name | ||
| setTimeout(() => { showShopCelebration.value = '' }, 2500) | ||
| applyItem(item) | ||
| async function buyItem(item: any) { | ||
| if (stats.value.xp >= item.cost) { | ||
| try { | ||
| // await updateExp(-item.cost) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove if shop/unlock now handles exp update |
||
| if (student.value?.id) { | ||
| await $fetch('/api/shop/unlock', { | ||
| method: 'POST', | ||
| body: { | ||
| studentId: student.value.id, | ||
| shopItemId: item.id, | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| await loadShopItems() | ||
| item.owned = true | ||
| showShopCelebration.value = item.name | ||
| setTimeout(() => { showShopCelebration.value = '' }, 2500) | ||
| await applyItem(item) | ||
| await restoreStudent() | ||
| } catch (e) { | ||
| console.error('Failed to purchase item', e) | ||
| alert('Purchase failed. Please try again.') | ||
| } | ||
| } else { | ||
| alert('Not enough XP!') | ||
| } | ||
| } | ||
|
|
||
| function applyItem(item: any) { | ||
| async function applyItem(item: any) { | ||
| if (item.type === 'theme') { | ||
| settings.theme = item.class | ||
| await saveSettings({ theme: item.class }) | ||
| return | ||
| } else if (item.type === 'animation') { | ||
| if (activeAnimations.value.includes(item.name)) { | ||
| activeAnimations.value = activeAnimations.value.filter(a => a !== item.name) | ||
|
|
@@ -114,9 +182,9 @@ function applyItem(item: any) { | |
| <span v-for="id in flyTickets" :key="id" class="absolute text-lg animate-ticket-fly pointer-events-none" | ||
| style="left:50%;top:50%;transform:translateX(-50%) translateY(-50%)">🎟️</span> | ||
| </div> | ||
| <NuxtLink to="/reader" | ||
| <NuxtLink to="/reader/settings" | ||
| class="w-14 h-14 bg-white/90 backdrop-blur-md rounded-xl flex items-center justify-center text-2xl border-2 border-white shadow-xl hover:scale-110 active:scale-95 transition-all" | ||
| style="text-decoration:none">🏠</NuxtLink> | ||
| style="text-decoration:none">⚙️</NuxtLink> | ||
| </div> | ||
| </header> | ||
|
|
||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'light' to 'default' again |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see other comment about changing 'light' to 'default' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| Warnings: | ||
|
|
||
| - You are about to drop the `_ShopItemToStudent` table. If the table is not empty, all the data it contains will be lost. | ||
|
|
||
| */ | ||
| -- DropTable | ||
| PRAGMA foreign_keys=off; | ||
| DROP TABLE "_ShopItemToStudent"; | ||
| PRAGMA foreign_keys=on; | ||
|
|
||
| -- CreateTable | ||
| CREATE TABLE "StudentShopItem" ( | ||
| "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||
| "studentId" INTEGER NOT NULL, | ||
| "shopItemId" INTEGER NOT NULL, | ||
| "unlockedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| CONSTRAINT "StudentShopItem_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
| CONSTRAINT "StudentShopItem_shopItemId_fkey" FOREIGN KEY ("shopItemId") REFERENCES "ShopItem" ("id") ON DELETE CASCADE ON UPDATE CASCADE | ||
| ); | ||
|
|
||
| -- CreateIndex | ||
| CREATE INDEX "StudentShopItem_studentId_idx" ON "StudentShopItem"("studentId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE INDEX "StudentShopItem_shopItemId_idx" ON "StudentShopItem"("shopItemId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "StudentShopItem_studentId_shopItemId_key" ON "StudentShopItem"("studentId", "shopItemId"); |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. coach implementation's admin change makes the seed script fail before it can seed all of the themes since the admin table no longer exists. |
Uh oh!
There was an error while loading. Please reload this page.