feat: add wishlist
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
subtitle?: string | null
|
||||
}>()
|
||||
|
||||
const background = computed(() => {
|
||||
let hue = 0
|
||||
for (const char of props.title) hue = (hue * 31 + char.codePointAt(0)!) % 360
|
||||
return `linear-gradient(155deg, hsl(${hue} 38% 40%), hsl(${(hue + 24) % 360} 42% 22%))`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative size-full overflow-hidden text-white [container-type:inline-size]"
|
||||
:style="{ background }"
|
||||
role="img"
|
||||
:aria-label="`Cover of ${title}`"
|
||||
>
|
||||
<span class="absolute inset-y-0 left-[7cqw] w-px bg-white/25" aria-hidden="true" />
|
||||
<div class="flex size-full flex-col justify-between gap-1 p-[8cqw] pl-[12cqw]">
|
||||
<span class="font-serif text-[clamp(0.5rem,11cqw,1.05rem)] leading-tight font-semibold text-balance wrap-anywhere line-clamp-6">
|
||||
{{ title }}
|
||||
</span>
|
||||
<span v-if="subtitle" class="text-[clamp(0.4rem,7cqw,0.7rem)] leading-tight tracking-wide text-white/75 uppercase wrap-anywhere line-clamp-2">
|
||||
{{ subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entry } from '~/types/entry'
|
||||
|
||||
const props = defineProps<{ entry: Entry, search: string }>()
|
||||
|
||||
const COLLAPSE_AFTER_CHARS = 320
|
||||
const COLLAPSE_AFTER_LINES = 5
|
||||
|
||||
const { showCovers } = useAppConfig()
|
||||
const expanded = ref(false)
|
||||
|
||||
const searching = computed(() => searchTerms(props.search).length > 0)
|
||||
const isLong = computed(() =>
|
||||
props.entry.text.length > COLLAPSE_AFTER_CHARS || props.entry.text.split('\n').length > COLLAPSE_AFTER_LINES
|
||||
)
|
||||
const collapsed = computed(() => isLong.value && !expanded.value && !searching.value)
|
||||
const wordCount = computed(() => props.entry.text.split(/\s+/).filter(Boolean).length)
|
||||
const parts = computed(() => highlight(props.entry.text, props.search))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
v-if="showCovers && entry.kind === 'review'"
|
||||
class="aspect-2/3 w-16 shrink-0 self-start overflow-hidden rounded shadow-sm ring-1 ring-default"
|
||||
>
|
||||
<CoverTile :title="entry.review.title" :subtitle="entry.review.author" />
|
||||
</div>
|
||||
|
||||
<article class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 items-baseline gap-3">
|
||||
<span class="shrink-0 text-xs text-muted tabular-nums">{{ formatDate(entry.date) }}</span>
|
||||
<p v-if="entry.kind === 'review'" class="min-w-0 truncate">
|
||||
<span class="font-medium text-highlighted">{{ entry.review.title }}</span>
|
||||
<span v-if="entry.review.author" class="ml-3 text-sm text-muted">{{ entry.review.author }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="mt-1.5 text-sm leading-relaxed whitespace-pre-line text-default"
|
||||
:class="{ 'line-clamp-5': collapsed }"
|
||||
><template v-for="(part, index) in parts" :key="index"><mark v-if="part.hit" class="rounded-sm bg-primary/30 px-0.5 text-highlighted">{{ part.text }}</mark><template v-else>{{ part.text }}</template></template></p>
|
||||
|
||||
<UButton
|
||||
v-if="isLong && !searching"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
class="mt-1 px-0"
|
||||
:label="expanded ? 'Show less' : `Read more (${wordCount} words)`"
|
||||
@click="expanded = !expanded"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const { open, close } = useLog()
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Note', value: 'note', icon: NOTE_ICON, slot: 'note' as const },
|
||||
{ label: 'Review', value: 'review', icon: REVIEW_ICON, slot: 'review' as const }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model:open="open" title="Log">
|
||||
<template #body>
|
||||
<UTabs :items="tabs" default-value="note" class="w-full" :ui="{ content: 'pt-4' }">
|
||||
<template #note>
|
||||
<NoteForm @saved="close()" />
|
||||
</template>
|
||||
<template #review>
|
||||
<ReviewForm @saved="close()" />
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
|
||||
const state = reactive({ date: todayIso(), text: '' })
|
||||
const { saving, save } = useSaveEntry({ url: '/api/notes', dataKey: 'notes', noun: 'note', onSaved: () => emit('saved') })
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<NewNoteInput>) {
|
||||
return save(event.data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm :schema="newNoteSchema" :state="state" class="space-y-5" @submit="onSubmit">
|
||||
<UFormField label="Date" name="date" required>
|
||||
<UInput v-model="state.date" type="date" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Note" name="text" required>
|
||||
<UTextarea v-model="state.text" :rows="8" autoresize :maxrows="18" class="w-full" autofocus />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" label="Save note" :icon="NOTE_ICON" :loading="saving" />
|
||||
</UForm>
|
||||
</template>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{ unlocked: [] }>()
|
||||
|
||||
const password = ref('')
|
||||
const error = ref<string>()
|
||||
const unlocking = ref(false)
|
||||
|
||||
async function unlock() {
|
||||
if (!password.value) return
|
||||
unlocking.value = true
|
||||
error.value = undefined
|
||||
try {
|
||||
await $fetch('/api/login', { method: 'POST', body: { password: password.value } })
|
||||
emit('unlocked')
|
||||
} catch (e) {
|
||||
error.value = errorMessage(e) ?? 'Couldn\'t unlock the diary'
|
||||
password.value = ''
|
||||
} finally {
|
||||
unlocking.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="true" :dismissible="false" :close="false" title="Diary" description="Enter your password to open the diary.">
|
||||
<template #body>
|
||||
<form class="space-y-4" @submit.prevent="unlock">
|
||||
<UFormField label="Password" :error="error">
|
||||
<UInput v-model="password" type="password" autocomplete="current-password" class="w-full" autofocus />
|
||||
</UFormField>
|
||||
<UButton type="submit" label="Unlock" icon="i-lucide-lock-open" :loading="unlocking" :disabled="!password" />
|
||||
</form>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
|
||||
const mediumOptions = MEDIUM_KEYS.map(medium => ({
|
||||
label: MEDIUMS[medium].label,
|
||||
value: medium,
|
||||
icon: MEDIUM_ICONS[medium]
|
||||
}))
|
||||
|
||||
const state = reactive({ medium: 'book' as Medium, date: todayIso(), title: '', author: '', text: '' })
|
||||
const { saving, save } = useSaveEntry({ url: '/api/reviews', dataKey: 'reviews', noun: 'review', onSaved: () => emit('saved') })
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<NewReviewInput>) {
|
||||
return save(event.data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm :schema="newReviewSchema" :state="state" class="space-y-5" @submit="onSubmit">
|
||||
<UFormField label="Medium" name="medium" required>
|
||||
<URadioGroup v-model="state.medium" :items="mediumOptions" orientation="horizontal" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Date" name="date" required>
|
||||
<UInput v-model="state.date" type="date" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Title" name="title" required>
|
||||
<UInput v-model="state.title" class="w-full" autofocus />
|
||||
</UFormField>
|
||||
|
||||
<UFormField :label="MEDIUMS[state.medium].creator" name="author">
|
||||
<UInput v-model="state.author" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Review" name="text" required>
|
||||
<UTextarea v-model="state.text" :rows="8" autoresize :maxrows="18" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" label="Save review" :icon="REVIEW_ICON" :loading="saving" />
|
||||
</UForm>
|
||||
</template>
|
||||
Reference in New Issue
Block a user