Skip to content

Commit

Permalink
Allow overwriting previously saved wheels
Browse files Browse the repository at this point in the history
  • Loading branch information
gomander committed Jan 12, 2024
1 parent 5af2bc0 commit 1bce965
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 61 deletions.
116 changes: 87 additions & 29 deletions src/lib/components/SaveCloudDialog.svelte
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
<script lang="ts">
import { onMount } from 'svelte'
import {
ProgressRadial, getModalStore, getToastStore
} from '@skeletonlabs/skeleton'
import wheelStore from '$lib/stores/WheelStore'
import { getCurrentUser } from '$lib/utils/Firebase'
import { createWheel } from '$lib/utils/Api'
import { createWheel, getWheel, updateWheel } from '$lib/utils/Api'
import { toastDefaults } from '$lib/utils/Toast'
const modalStore = getModalStore()
const toastStore = getToastStore()
let title = $wheelStore.config.title
let loading = false
let saveMode: 'overwrite' | 'new' = 'new'
const user = getCurrentUser()
if (!user) {
modalStore.close()
modalStore.trigger({
type: 'component',
component: 'loginDialog',
meta: { next: 'saveCloudDialog' }
})
}
onMount(async () => {
const user = getCurrentUser()
if (!user) {
modalStore.close()
modalStore.trigger({
type: 'component',
component: 'loginDialog',
meta: { next: 'saveCloudDialog' }
})
return
}
if ($wheelStore.path) {
loading = true
try {
const response = await getWheel($wheelStore.path, user.uid)
if (!response.success) {
wheelStore.setPath(null)
return
}
saveMode = 'overwrite'
} catch (error) {
wheelStore.setPath(null)
} finally {
loading = false
}
}
})
const save = async () => {
if (loading) return
Expand All @@ -30,19 +50,34 @@
if (!title) {
throw new Error('Title is required')
}
const user = getCurrentUser()
if (!user) {
throw new Error('User is not logged in')
}
const response = await createWheel({
wheel: {
config: { ...$wheelStore.config, title },
entries: $wheelStore.entries
},
visibility: 'private',
uid: user.uid
}, user.uid)
if (!response.success) {
throw new Error('Failed to save wheel')
if (saveMode === 'new') {
const response = await createWheel({
wheel: {
config: { ...$wheelStore.config, title },
entries: $wheelStore.entries
},
visibility: 'private',
uid: user.uid
}, user.uid)
if (!response.success) {
throw new Error('Failed to save wheel')
}
}
if (saveMode === 'overwrite' && $wheelStore.path) {
const response = await updateWheel($wheelStore.path, {
wheel: {
config: { ...$wheelStore.config, title },
entries: $wheelStore.entries
},
uid: user.uid
}, user.uid)
if (!response.success) {
throw new Error('Failed to save wheel')
}
}
modalStore.close()
toastStore.trigger({
Expand All @@ -63,14 +98,10 @@
}
}
// TODO: If the wheel has been saved previously, present the user with a
// choice to overwrite the existing wheel or create a new wheel. This can be
// done by writing the path to the wheel store when saving or opening a wheel
// and checking if the path exists when opening the save cloud dialog. The
// user should be shown as much metadata about the wheel as possible, such as
// the title, number of entries, and date created. Additionally, there should
// be a thumbnail of the wheel, the same one that would be shown in the open
// cloud dialog.
// TODO: When wheelStore has a path, the user should be shown as much metadata
// about the wheel as possible, such as the title, number of entries, and date
// created. Additionally, there should be a thumbnail of the wheel, the same
// one that would be shown in the open cloud dialog.
</script>

{#if $modalStore[0]}
Expand All @@ -84,9 +115,36 @@
on:submit|preventDefault={save}
class="flex flex-col gap-4"
>
{#if $wheelStore.path}
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
type="radio"
name="saveMode"
value="overwrite"
bind:group={saveMode}
class="radio"
/>
<span>
Overwrite "{$wheelStore.config.title}" ({$wheelStore.path})
</span>
</label>
<label class="flex items-center gap-2">
<input
type="radio"
name="saveMode"
value="new"
bind:group={saveMode}
checked
class="radio"
/>
<span>Save a new wheel</span>
</label>
</div>
{/if}

<label class="label">
<span class="required">Title</span>

<input
type="text"
maxlength="50"
Expand Down
30 changes: 30 additions & 0 deletions src/lib/server/FirebaseAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,36 @@ export const saveWheel = async (
return path
}

export const updateWheel = async (
path: string,
wheel: Partial<Omit<ApiWheel, 'path'>>,
uid: string,
visibility?: WheelVisibility
) => {
const wheelMetaDoc = db.doc(`wheel-meta/${path}`)
const wheelMetaSnap = await wheelMetaDoc.get()
if (!wheelMetaSnap.exists) {
return null
}
const wheelMeta = wheelMetaSnap.data() as ApiWheelMeta
if (wheelMeta.uid !== uid) {
return null
}
const newWheelMeta: Partial<ApiWheelMeta> = {
updated: Date.now()
}
if (wheel.config && wheel.config.title !== wheelMeta.title) {
newWheelMeta.title = wheel.config.title
}
if (visibility) {
newWheelMeta.visibility = visibility
}
await wheelMetaDoc.update(newWheelMeta)
const wheelDoc = db.doc(`wheels/${path}`)
await wheelDoc.update({ ...wheel } satisfies Partial<ApiWheel>)
return wheelMeta.path
}

const getNewWheelPath = async () => {
let path: string
let snap: FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/WheelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const createWheelStore = (state: WheelStoreData) => {
})
}

const setPath = (path: string) => {
const setPath = (path: string | null) => {
update(state => {
state.path = path
return state
Expand Down
27 changes: 27 additions & 0 deletions src/lib/utils/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ export const createWheel = async (
return await response.json() as ApiResponse<{ path: string }>
}

export const updateWheel = async (
path: string,
data: UpdateWheelData,
uid?: string | null,
apiKey?: string | null,
fetch = window.fetch
) => {
const headers: HeadersInit = { 'Content-Type': 'application/json' }
if (uid) {
headers.authorization = uid
}
if (apiKey) {
headers['x-api-key'] = apiKey
}
const response = await fetch(
`/api/wheels/${path}`,
{
method: 'PUT',
headers,
body: JSON.stringify(data)
}
)
return await response.json() as ApiResponse<{ path: string }>
}

export const getWheel = async (
path: string,
uid?: string | null,
Expand Down Expand Up @@ -97,3 +122,5 @@ export interface CreateWheelData {
visibility: WheelVisibility
uid: string
}

export type UpdateWheelData = Partial<CreateWheelData> & { uid: string }
71 changes: 44 additions & 27 deletions src/lib/utils/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,53 @@ import { wheelTypes, hubSizeKeys, defaultColors } from '$lib/utils/WheelConfig'
import { confettiTypes } from '$lib/utils/ConfettiLauncher'
import { wheelVisibilities } from '$lib/utils/Api'

export const wheelSchema = z.object({
const wheelConfigSchema = z.object({
type: z.optional(z.enum(wheelTypes)).default('color'),
title: z.string().min(1).max(50).trim(),
description: z.optional(z.string().max(200).trim()).default(''),
spinTime: z.optional(z.number().min(1).max(60)).default(10),
indefiniteSpin: z.optional(z.boolean()).default(false),
colors: z.optional(
z.array(z.string().trim().length(7).startsWith('#'))
).default(defaultColors),
confetti: z.optional(z.enum(confettiTypes)).default('off'),
displayWinnerDialog: z.optional(z.boolean()).default(true),
winnerMessage: z.optional(z.string().max(50).trim()).default(''),
hubSize: z.optional(z.enum(hubSizeKeys)).default('S'),
duringSpinSound: z.optional(z.string().max(100).trim()).default(''),
duringSpinSoundVolume: z.optional(z.number().min(0).max(1)).default(0),
afterSpinSound: z.optional(z.string().max(100).trim()).default(''),
afterSpinSoundVolume: z.optional(z.number().min(0).max(1)).default(0),
image: z.optional(z.string().trim()).default('')
})

const entrySchema = z.object({
text: z.string(),
id: z.optional(z.string().trim().length(8)).default(getNewEntryId())
})

const entriesSchema = z.array(entrySchema)

export const uidValidator = z.string().trim().min(20).max(40)

export const createWheelSchema = z.object({
wheel: z.object({
config: z.object({
type: z.optional(z.enum(wheelTypes)).default('color'),
title: z.string().min(1).max(50).trim(),
description: z.optional(z.string().max(200).trim()).default(''),
spinTime: z.optional(z.number().min(1).max(60)).default(10),
indefiniteSpin: z.optional(z.boolean()).default(false),
colors: z.optional(
z.array(z.string().trim().length(7).startsWith('#'))
).default(defaultColors),
confetti: z.optional(z.enum(confettiTypes)).default('off'),
displayWinnerDialog: z.optional(z.boolean()).default(true),
winnerMessage: z.optional(z.string().max(50).trim()).default(''),
hubSize: z.optional(z.enum(hubSizeKeys)).default('S'),
duringSpinSound: z.optional(z.string().max(100).trim()).default(''),
duringSpinSoundVolume: z.optional(z.number().min(0).max(1)).default(0),
afterSpinSound: z.optional(z.string().max(100).trim()).default(''),
afterSpinSoundVolume: z.optional(z.number().min(0).max(1)).default(0),
image: z.optional(z.string().trim()).default('')
}),
entries: z.array(
z.object({
text: z.string(),
id: z.optional(z.string().trim().length(8)).default(getNewEntryId())
})
)
config: wheelConfigSchema,
entries: entriesSchema
}),
visibility: z.enum(wheelVisibilities),
uid: z.string().trim().min(20).max(40)
uid: uidValidator
})

export const updateWheelSchema = z.object({
wheel: z.optional(
z.object({
config: z.optional(wheelConfigSchema),
entries: z.optional(entriesSchema)
})
).default({}),
visibility: z.optional(z.enum(wheelVisibilities)),
uid: uidValidator
})

export const emailValidator = z.string().email()
Expand Down
4 changes: 2 additions & 2 deletions src/routes/api/wheels/+server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod'
import { SVELTE_WHEEL_API_KEY } from '$env/static/private'
import { getUserWheelsMeta, saveWheel } from '$lib/server/FirebaseAdmin'
import { wheelSchema } from '$lib/utils/Schemas'
import { createWheelSchema } from '$lib/utils/Schemas'
import type { ApiError, ApiSuccess, ApiWheelMeta } from '$lib/utils/Api'

export const GET = async ({ request }) => {
Expand Down Expand Up @@ -52,7 +52,7 @@ export const POST = async ({ request }) => {
}
try {
const body = await request.json()
const data = wheelSchema.parse(body)
const data = createWheelSchema.parse(body)
const path = await saveWheel(data.wheel, data.uid, data.visibility)
return new Response(
JSON.stringify({
Expand Down
Loading

0 comments on commit 1bce965

Please sign in to comment.