18 Commits

Author SHA1 Message Date
Alexis
674a38c30d Fixed search shortcut 2025-08-24 11:38:10 +02:00
Alexis
e7cef314a1 Fixed untranslated string 2025-08-24 11:26:11 +02:00
Alexis
806d3a7908 Updated package 2025-08-23 22:45:35 +02:00
Alexis
6251499940 Added sheet for sidebar mobile 2025-08-15 15:37:35 +02:00
Alexis
f3d5db76ac Removed sidebar toggle on public pages 2025-08-12 11:18:47 +02:00
Alexis
08620b5044 Made content body smaller 2025-08-12 11:16:52 +02:00
Alexis
f89cc22e87 Fixed quick create dialog not resetting skeleton 2025-08-12 11:09:28 +02:00
Alexis
51c678d27e Fixed dashboard link hitbox 2025-08-12 10:44:56 +02:00
Alexis
b9b891c61f Fixed nav colors in dark mode 2025-08-12 10:43:49 +02:00
Alexis
df06228e68 Fixed transition render issue 2025-08-12 10:37:37 +02:00
Alexis
44193aa018 Added about link to CTA 2025-08-12 10:34:23 +02:00
Alexis
73eceda96c Changed secondary button color 2025-08-12 10:18:07 +02:00
Alexis
e305b21f80 Changed projects page handle 2025-08-12 10:11:28 +02:00
Alexis
d2b66a42dd Fixed dark mode select hover 2025-08-12 10:07:12 +02:00
Alexis
be32a018c6 Added input focus on entity forms 2025-08-11 20:23:25 +02:00
Alexis
a5a232bac7 Added navbar on public pages 2025-08-11 20:00:04 +02:00
Alexis
20045b125e Changed explore page h2 2025-08-09 21:06:42 +02:00
Alexis
f1a3854d1d Merge branch 'feat/drag-events' into dev 2025-08-09 18:56:57 +02:00
42 changed files with 2495 additions and 1834 deletions

View File

@@ -40,23 +40,19 @@
}
}
p, ul, ol {
font-size: var(--text-sm);
}
/* Normal link */
p a:not([class]) {
color: var(--color-sky-600);
color: var(--color-primary);
text-underline-offset: .25rem;
&:hover {
color: var(--color-sky-500);
color: var(--color-accent);
text-decoration: underline;
}
@variant dark {
color: var(--color-teal-500);
&:hover {
color: var(--color-teal-300);
}
}
}
ul {

View File

@@ -56,7 +56,7 @@
--primary: rgb(101, 230, 166);
--primary-foreground: hsl(180 0 4.9%);
--secondary: hsl(222.2 47.4% 11.2%);
--secondary: hsl(0, 0%, 22.5%);
--secondary-foreground: hsl(210 40% 98%);
--accent: hsl(11, 87%, 69%);

View File

@@ -6,6 +6,13 @@ import { VisuallyHidden } from "radix-vue";
const isDialogOpen = ref<boolean>(false);
const { resetSkeleton } = useCalendar();
// Watch the popover state
watch(isDialogOpen, (hasOpened, _o) => {
if (hasOpened) {
resetSkeleton()
}
})
// Toggles the dialog
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
@@ -45,7 +52,7 @@ const breakpoints = useBreakpoints(
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
:trap-focus="true"
@escape-key-down="handleClosing"
@pointer-down-outside.prevent="handleClosing"
@pointer-down-outside="handleClosing"
>
<UiDialogTitle class="max-md:mb-8">
{{ $t("entity.calendar.event.addSingle") }}

View File

@@ -29,7 +29,7 @@ function handleClosing() {
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="(e) => e.preventDefault()"
@pointer-down-outside.prevent
>
<header class="pl-8 grid gap-y-2 max-md:mb-8">
<UiDialogTitle>

View File

@@ -16,6 +16,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/**
* === Current date ===
*/
@@ -103,6 +106,7 @@ function handleFormCancel() {
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
ref="newCalendarName"
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"

View File

@@ -17,6 +17,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/**
* === Form Validation ===
*/
@@ -85,6 +88,7 @@ function handleFormCancel() {
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
ref="newCalendarName"
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"

View File

@@ -195,10 +195,9 @@ function handleEntitySwitch() {
// Key combos to deploy modal
const keys = useMagicKeys()
const controlPeriod = computed(() => keys.control_period?.value)
whenever(keys.control_period, () => {
openUiDialog()
})
whenever(controlPeriod, openUiDialog)
const searchResultsRef = ref<HTMLElement | null>(null)
const { y: searchResultsY } = useScroll(searchResultsRef)

View File

@@ -188,7 +188,12 @@ async function handleTileDrop() {
<button
class="text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:text-foreground hover:bg-foreground/10"
>
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
<template v-if="eventsNotDisplayed === 1">
{{ $t('entity.calendar.event.oneOtherEvent') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.multipleOtherEvents', { count: eventsNotDisplayed }) }}
</template>
</button>
</UiPopoverTrigger>
<UiPopoverContent

View File

@@ -0,0 +1,39 @@
<template>
<header role="banner" class="sticky top-0 border-b-1 border-border py-4 px-2 z-50 backdrop-blur">
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between">
<div class="md:flex-1 flex justify-start" />
<nav class="md:flex-1">
<menu class="text-sm flex items-center justify-center gap-4">
<li>
<NuxtLink to="/" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
{{ $t('breadcrumbs.home') }}
</NuxtLink>
</li>
<li>
<NuxtLink to="/explore" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
{{ $t('pages.explore.menuLabel') }}
</NuxtLink>
</li>
<li>
<NuxtLink to="/about" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
{{ $t('pages.about.menuLabel') }}
</NuxtLink>
</li>
</menu>
</nav>
<div class="md:flex-1 flex justify-end">
<UserDashboardLink size="xs" />
</div>
</div>
</div>
</header>
</template>
<style lang="scss" scoped>
header {
background-color: color-mix(in srgb, var(--color-background) 75%, transparent);
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { PhInfo } from '@phosphor-icons/vue';
</script>
<template>
<div class="flex justify-between items-center gap-2">
<div class="grow">
<UserCTA />
</div>
<UiButton size="icon" class="size-8 hover:bg-secondary hover:text-secondary-foreground" variant="ghost" as-child>
<NuxtLink to="/about">
<PhInfo size="18" />
</NuxtLink>
</UiButton>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { PanelsLeftBottom } from 'lucide-vue-next';
</script>
<template>
<menu class="min-w-32 pt-16 flex flex-col gap-0.5">
<li class="w-full">
<UiButton
variant="ghost"
size="sm"
class="h-8 gap-1.5 hover:bg-secondary w-full justify-start"
as-child
>
<NuxtLink to="/my" active-class="active-link" exact-active-class="exact-link">
<PanelsLeftBottom :size="18" />
<span class="text-[.9em]">
{{ $t('ui.sidebarMenu.projects') }}
</span>
</NuxtLink>
</UiButton>
</li>
</menu>
</template>
<style lang="scss" scoped>
.active-link {
color: var(--color-secondary-foreground);
background-color: var(--color-secondary);
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
}
}
.exact-link {
color: var(--color-primary-foreground);
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
cursor: initial;
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
}
}
.dark .exact-link {
background-color: var(--color-primary);
}
</style>

View File

@@ -1,136 +1,32 @@
<script lang="ts" setup>
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhInfo, PhX } from "@phosphor-icons/vue"
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
import { PanelsLeftBottom } from "lucide-vue-next"
import { cn } from "@/lib/utils";
import { breakpointsTailwind } from "@vueuse/core"
import { PhInfo } from "@phosphor-icons/vue";
const { revealAdvancedSearch } = useCalendar()
const { toggleSidebar } = useUiStore()
const { currentMenu, isSidebarOpened } = storeToRefs(useUiStore())
function handleMenuItemAction(actionType: SidebarMenuActionType) {
if (actionType === "event-search") {
revealAdvancedSearch()
}
}
function computeMenuItemIcon(iconString: SidebarMenuIcon) {
switch (iconString) {
case "universe":
return PhHurricane
case "world":
return PhGlobeHemisphereEast
default:
return PhCompass
}
}
const breakpoints = useBreakpoints(
breakpointsTailwind
)
// const sidebarRef = ref(null)
// onClickOutside(sidebarRef, () => {
// isSidebarOpened.value = false
// })
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template>
<nav
ref="sidebarRef"
:class="cn(
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center md:transition-colors'], // Base appearance
['max-md:hidden w-fit relative isolate p-2 grid gap-4 grid-rows-[1fr_auto] bg-background justify-center transition-colors'], // Base appearance
['after:opacity-50 after:contrast-125 dark:after:opacity-75'], // After styling
['border-r-[1px] border-r-border dark:border-r-border'], // Colours
['max-md:justify-stretch max-md:px-4 max-md:py-4 max-md:absolute max-md:left-0 max-md:inset-0 max-md:z-50 max-md:w-40 max-md:max-w-full max-md:transition-all'], // Responsive behaviours
{
'max-md:-translate-x-40': !isSidebarOpened,
'max-md:-translate-x-0': isSidebarOpened
}
)"
>
<menu class="flex flex-col gap-4 max-md:items-center">
<li class="mb-12 mt-4 max-md:self-start">
<UiButton
variant="outline"
size="icon"
class="md:hidden size-9 border-background/30"
@click="toggleSidebar"
>
<PhX size="19" />
</UiButton>
</li>
<SidebarMenu />
<li class="max-md:self-start">
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-full max-md:hidden"
as-child
>
<NuxtLink to="/explore">
<PhCompass size="24" weight="fill" />
</NuxtLink>
</UiButton>
<NuxtLink
to="/explore"
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
>
<PhCompass size="22" weight="fill" />
<span class="text-[.9em]">
{{ $t('pages.explore.menuLabel') }}
</span>
</NuxtLink>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>
{{ $t('pages.explore.menuLabel') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
<ClientOnly>
<li v-for="(item, i) in currentMenu" :key="i">
<UiTooltipProvider :delay-duration="50">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
v-if="item.to"
variant="ghost"
size="icon"
class="rounded-full"
as-child
>
<NuxtLink :to="item.to">
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</NuxtLink>
</UiButton>
<UiButton
v-if="item.action"
variant="ghost"
size="icon"
class="rounded-full"
@click="handleMenuItemAction(item.action!)"
>
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>{{ item.tooltip }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
</ClientOnly>
</menu>
<UserCTA />
<SidebarFooter />
</nav>
<UiSheet v-model:open="isSidebarOpened">
<UiSheetContent side="left" class="p-2 grid gap-4 grid-rows-[auto_1fr]">
<UiSheetHeader />
<SidebarMenu />
<SidebarFooter />
</UiSheetContent>
</UiSheet>
</template>

View File

@@ -8,8 +8,9 @@ const { toggleSidebar } = useUiStore()
<UiButton
size="icon"
variant="outline"
class="size-7 border-border hover:bg-secondary hover:text-secondary-foreground"
@click="toggleSidebar"
>
<PhList size="19" weight="light" />
<PhList size="14" weight="light" />
</UiButton>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed } from "vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle, PhInfo } from "@phosphor-icons/vue"
import { cn } from "@/lib/utils";
const router = useRouter()
@@ -44,46 +44,43 @@ async function handleLogout() {
console.log(error.message)
}
}
type AvailableRoutes = "/my" | "/my/settings"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script>
<template>
<ClientOnly>
<UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger class="aspect-square">
<TransitionGroup name="fade-group" appear>
<UiAvatar
v-if="user"
id="user-avatar"
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent transition-all cursor-pointer"
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
>
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<UiButton
v-else
variant="outline"
size="icon"
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent border-none dark:bg-background rounded-full bg-primary transition-all cursor-pointer"
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
>
<PhUserCircle size="24" />
</UiButton>
</TransitionGroup>
<UiDropdownMenuTrigger as-child>
<UiButton
variant="ghost"
class="w-full justify-start p-2 h-fit hover:text-secondary-foreground hover:bg-secondary"
:class="cn({ 'bg-secondary': menuOpened })"
>
<TransitionGroup name="fade-group" appear>
<div
v-if="user"
class="flex gap-2 items-center"
>
<UiAvatar
id="user-avatar"
size="sm"
class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
>
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<div class="text-xs">
{{ userMeta?.name }}
</div>
</div>
</TransitionGroup>
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
@@ -91,15 +88,6 @@ function pushRoute(to: AvailableRoutes) {
<p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.greeting', { user: user?.email }) }}
</p>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
<PhUser size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.profile') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
</template>
<template v-else>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const profileUrl: string = `${useRequestURL().origin}/my/`
interface Props extends PrimitiveProps {
size?: ButtonVariants["size"]
}
defineProps<Props>()
async function handleGoogleLogin() {
const { error } = await auth.signInWithOAuth({
provider: "google",
options: {
queryParams: {
access_type: "offline",
prompt: "consent"
},
redirectTo: profileUrl
}
})
if (error) {
console.log(error.message)
}
}
</script>
<template>
<UiButton v-if="user" :size as-child>
<NuxtLink to="/my">
Dashboard
</NuxtLink>
</UiButton>
<UiButton v-else @click="handleGoogleLogin" :size>
Log in
</UiButton>
</template>

View File

@@ -9,9 +9,9 @@ export const avatarVariant = cva(
{
variants: {
size: {
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
sm: "size-6 text-xs",
base: "size-14 text-2xl",
lg: "size-24 text-5xl",
},
shape: {
circle: "rounded-full",

View File

@@ -10,7 +10,7 @@ export const buttonVariants = cva(
default: "bg-primary text-primary-foreground hover:bg-primary/70",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-primary hover:text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/20",
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/40",
ghost: "hover:bg-primary/70 hover:text-primary-foreground",
link: "text-primary underline-offset-4 hover:underline"
},

View File

@@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
:class="
cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background hover:bg-secondary hover:text-primary-foreground px-3 py-2 text-sm ring-offset-background transition-colors placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background hover:bg-secondary hover:text-primary-foreground dark:hover:text-primary px-3 py-2 text-sm ring-offset-background transition-colors placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
data-slot="sheet"
v-bind="forwarded"
>
<slot />
</DialogRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full border-l max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-full border-r max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot />
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import {
DialogClose
} from "reka-ui"
import { PhX } from "@phosphor-icons/vue";
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 disabled:pointer-events-none"
as-child
>
<UiButton
size="icon"
variant="outline"
class="size-7 border-border bg-background hover:bg-secondary hover:text-secondary-foreground"
>
<PhX size="14" />
<span class="sr-only">{{ $t('ui.actions.close') }}</span>
</UiButton>
</DialogClose>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/70', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,8 @@
export { default as Sheet } from "./Sheet.vue"
export { default as SheetClose } from "./SheetClose.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetFooter } from "./SheetFooter.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"
export { default as SheetTrigger } from "./SheetTrigger.vue"

View File

@@ -11,6 +11,9 @@ onMounted(() => {
worldSkeleton.value = { ...defaultWorld }
})
const newWorldName = shallowRef<HTMLInputElement>()
useFocus(newWorldName, { initialValue: true })
const isLoading = ref<boolean>(false)
/**
@@ -61,6 +64,7 @@ function handleFormCancel() {
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div class="grid gap-4">
<input
ref="newWorldName"
id="new-world-name"
v-model="worldSkeleton.name"
type="text"

View File

@@ -19,6 +19,9 @@ onMounted(() => {
worldSkeleton.value = { ...props.world } as World
})
const newWorldName = shallowRef<HTMLInputElement>()
useFocus(newWorldName, { initialValue: true })
const isLoading = ref<boolean>(false)
/**
@@ -75,6 +78,7 @@ function handleFormCancel() {
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div class="grid gap-4">
<input
ref="newWorldName"
id="new-world-name"
v-model="worldSkeleton.name"
type="text"

View File

@@ -60,38 +60,32 @@ switch (statusCode) {
<template>
<div class="h-screen">
<div class="h-full grid grid-cols-[auto_1fr] transition-colors">
<Sidebar />
<div class="h-full w-full grid place-items-center transition-colors">
<Head>
<Title>{{ $t(titleKey) }}</Title>
</Head>
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
<div class="h-full w-full grid place-items-center">
<Head>
<Title>{{ $t(titleKey) }}</Title>
</Head>
<div class="grid text-center justify-items-center opacity-80">
<PhImageBroken v-if="statusCode === 404" size="100" class="opacity-60" />
<PhLinkBreak v-else-if="statusCode === 500" size="100" class="opacity-60" />
<PhBugBeetle v-else size="100" class="opacity-60" />
<div class="grid text-center justify-items-center opacity-80">
<PhImageBroken v-if="statusCode === 404" size="100" class="opacity-60" />
<PhLinkBreak v-else-if="statusCode === 500" size="100" class="opacity-60" />
<PhBugBeetle v-else size="100" class="opacity-60" />
<Heading level="h0">
{{ $t(titleKey) }}
</Heading>
<Heading level="h0">
{{ $t(titleKey) }}
</Heading>
<div class="mt-6 md:text-lg">
<p>{{ $t(descriptionKey) }}</p>
<p>{{ $t(subDescriptionKey) }}</p>
</div>
<UiButton variant="default" class="mt-6 gap-2" as-child>
<RouterLink to="/">
<PhArrowBendDoubleUpLeft size="24" />
{{ $t('ui.backToHome') }}
</RouterLink>
</UiButton>
</div>
<div class="mt-6 md:text-lg">
<p>{{ $t(descriptionKey) }}</p>
<p>{{ $t(subDescriptionKey) }}</p>
</div>
<UiButton variant="default" class="mt-6 gap-2" as-child>
<RouterLink to="/">
<PhArrowBendDoubleUpLeft size="24" />
{{ $t('ui.backToHome') }}
</RouterLink>
</UiButton>
</div>
</div>
</div>

View File

@@ -1,16 +1,6 @@
<script lang="ts" setup>
import { cn } from '~/lib/utils';
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template>
<div
class="h-full grid md:grid-cols-[auto_1fr] bg-background transition-colors after:absolute after:transition-colors"
:class="cn({
'max-md:after:bg-transparent': isSidebarOpened,
'has-sidebar max-md:after:bg-black/20 md:after:opacity-0 md:after:pointer-events-none': isSidebarOpened
})"
>
<Sidebar />

View File

@@ -1,7 +1,36 @@
<template>
<div
class="wrapper max-h-screen h-full transition-all overflow-y-auto"
class="wrapper max-h-screen h-full transition-all overflow-y-auto grid grid-rows-[auto_1fr]"
>
<slot />
<Navbar />
<div class="inner-content">
<slot />
</div>
</div>
</template>
<style lang="scss" scoped>
.inner-content {
position: relative;
&::before {
position: absolute;
inset-inline: 0;
top: 0;
display: block;
content: '';
height: .1rem;
max-width: 100%;
background: linear-gradient(
90deg,
transparent 0%,
transparent 10%,
color-mix(in srgb, var(--color-primary) 50%, transparent) 25%,
color-mix(in srgb, var(--color-primary) 50%, transparent) 75%,
transparent 90%,
transparent 100%
);
}
}
</style>

View File

@@ -1,4 +1,5 @@
import { type ClassValue, clsx } from "clsx"
import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {

View File

@@ -20,20 +20,14 @@ watch(locale, () => refresh())
</Head>
<main class="overflow-y-auto py-8 px-5 md:px-8 grid gap-4 grid-rows-[auto_1fr]">
<div class="flex items-center gap-3">
<div class="md:hidden">
<SidebarToggle />
</div>
<Heading level="h1">
{{ $t("pages.about.title") }}
</Heading>
</div>
<Heading level="h1">
{{ $t("pages.about.title") }}
</Heading>
<div v-if="status === 'pending'" class="grid place-items-center">
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
</div>
<LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-5xl" />
<LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-4xl" />
</main>
</template>

View File

@@ -18,15 +18,9 @@ const { data: calendars, status } = useLazyAsyncData<{ data: Calendar[] }>("expl
</Head>
<Spacing size="lg">
<div class="flex items-center gap-3">
<div class="md:hidden">
<SidebarToggle />
</div>
<Heading level="h1">
{{ $t("pages.explore.title") }}
</Heading>
</div>
<Heading level="h1">
{{ $t("pages.explore.title") }}
</Heading>
<Spacing size="lg">
<Heading level="h2">

View File

@@ -6,27 +6,6 @@ useHead({
definePageMeta({
layout: "public"
})
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const profileUrl: string = `${useRequestURL().origin}/my/`
async function handleGoogleLogin() {
const { error } = await auth.signInWithOAuth({
provider: "google",
options: {
queryParams: {
access_type: "offline",
prompt: "consent"
},
redirectTo: profileUrl
}
})
if (error) {
console.log(error.message)
}
}
</script>
<template>
@@ -39,20 +18,15 @@ async function handleGoogleLogin() {
</Head>
<div class="py-8 md:py-24 px-4">
<div class="flow text-center max-w-2xl mx-auto">
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">A home for your creativity</h1>
<p class="text-sm">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsam eos rem accusantium voluptates? Inventore dolorum reprehenderit quibusdam consequatur, totam iusto velit neque dolor dicta possimus cum, deleniti, saepe expedita culpa!</p>
<div class="flow text-center max-w-4xl mx-auto">
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">
{{ $t('pages.home.h1') }}
</h1>
<p class="text-sm">
{{ $t('pages.home.tagline') }}
</p>
<TransitionGroup name="fade-group" appear>
<UiButton v-if="user" as-child>
<NuxtLink to="/my">
Dashboard
</NuxtLink>
</UiButton>
<UiButton v-else @click="handleGoogleLogin">
Log in
</UiButton>
</TransitionGroup>
<UserDashboardLink />
</div>
</div>
</main>

3
app/pages/my.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>

View File

@@ -1,17 +1,20 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "default",
"style": "new-york",
"typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/main.css",
"css": "app/assets/main.css",
"baseColor": "slate",
"cssVariables": true
"cssVariables": true,
"prefix": ""
},
"framework": "nuxt",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
}

View File

@@ -32,6 +32,7 @@ export default defineI18nConfig(() => ({
save: "Save",
delete: "Delete",
edit: "Edit",
close: "Close"
},
colors: {
label: "Color",
@@ -69,12 +70,14 @@ export default defineI18nConfig(() => ({
backToHome: "Back to home",
sidebarMenu: {
profile: "Profile",
projects: "Projects",
appearance: "Appearance",
language: "Language",
account: "Account",
login: "Log in",
logout: "Log out",
avatarFallback: "Profile",
about: "About",
},
dark: "Dark",
light: "Light",
@@ -138,7 +141,7 @@ export default defineI18nConfig(() => ({
world: {
nameSingular: "World",
namePlural: "Worlds",
backToMy: "Back to my universe",
backToMy: "Back to projects",
backToSingle: "Back to {world}",
addSingle: "Add a world",
addSingleFirst: "Add your first world !",
@@ -169,7 +172,7 @@ export default defineI18nConfig(() => ({
nameSingular: "Calendar",
namePlural: "Calendars",
namePublicSingular: "Public Calendar",
namePublicPlural: "Public Calendars",
namePublicPlural: "Calendars",
addSingle: "Add a calendar",
addSingleFirst: "Add your first calendar !",
notFound: "Calendar not found",
@@ -224,6 +227,8 @@ export default defineI18nConfig(() => ({
hiddenLabel: "Hide this event",
hiddenTooltip: "This event is visible only to game masters.",
addLocation: "Add a place",
oneOtherEvent: "1 other",
multipleOtherEvents: "{count} others",
prevPage: "Previous page with events",
nextPage: "Next page with events",
outOfBoundsTitle: "No next or previous events were found",
@@ -327,13 +332,17 @@ export default defineI18nConfig(() => ({
}
},
pages: {
home: {
h1: "A home for your creativity",
tagline: "Visualize your fantasy or sci-fi worlds with our interactive calendars"
},
explore: {
menuLabel: "Explore",
title: "Explore worlds",
},
profile: {
title: "{user} — My universe",
metaTitle: "My universe",
title: "{user} — Projects",
metaTitle: "Projects",
},
about: {
title: "About this app",
@@ -344,7 +353,7 @@ export default defineI18nConfig(() => ({
breadcrumbs: {
home: "Home",
explore: "Explore",
profile: "Universe",
profile: "Projects",
world: "World",
calendar: "Calendar"
}
@@ -378,6 +387,7 @@ export default defineI18nConfig(() => ({
save: "Sauvegarder",
delete: "Supprimer",
edit: "Modifier",
close: "Fermer"
},
colors: {
label: "Couleur",
@@ -415,12 +425,14 @@ export default defineI18nConfig(() => ({
backToHome: "Retourner à l'accueil",
sidebarMenu: {
profile: "Profil",
projects: "Projets",
appearance: "Apparence",
language: "Langue",
account: "Compte",
login: "Connexion",
logout: "Déconnexion",
avatarFallback: "Profil",
about: "À propos",
},
dark: "Sombre",
light: "Clair",
@@ -484,7 +496,7 @@ export default defineI18nConfig(() => ({
world: {
nameSingular: "Monde",
namePlural: "Mondes",
backToMy: "Retourner à mon univers",
backToMy: "Retourner à Projets",
backToSingle: "Retourner sur {world}",
addSingle: "Ajouter un monde",
addSingleFirst: "Ajouter votre premier monde !",
@@ -570,6 +582,8 @@ export default defineI18nConfig(() => ({
hiddenLabel: "Rendre l'évènement privé",
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
addLocation: "Ajouter un endroit",
oneOtherEvent: "1 autre",
multipleOtherEvents: "{count} autres",
prevPage: "Précédente page à évènements",
nextPage: "Prochaine page à évènements",
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
@@ -677,13 +691,17 @@ export default defineI18nConfig(() => ({
}
},
pages: {
home: {
h1: "Un espace pour votre créativité",
tagline: "Visualisez vos mondes fantasy ou de science-fiction grâce à nos calendriers interactifs"
},
explore: {
menuLabel: "Explorer",
title: "Explorer les mondes",
},
profile: {
title: "{user} — Mon univers",
metaTitle: "Mon univers",
title: "{user} — Projets",
metaTitle: "Projets",
},
about: {
title: "À propos",
@@ -694,7 +712,7 @@ export default defineI18nConfig(() => ({
breadcrumbs: {
home: "Accueil",
explore: "Explorer",
profile: "Univers",
profile: "Projets",
world: "Monde",
calendar: "Calendrier"
}

View File

@@ -15,53 +15,55 @@
"dependencies": {
"@intlify/message-compiler": "^11.1.11",
"@nuxt/content": "3.6.3",
"@nuxt/eslint": "^1.8.0",
"@nuxt/eslint": "^1.9.0",
"@nuxt/image": "1.11.0",
"@nuxthub/core": "^0.9.0",
"@nuxtjs/i18n": "^10.0.3",
"@nuxtjs/i18n": "^10.0.6",
"@phosphor-icons/vue": "^2.2.1",
"@pinia/nuxt": "^0.11.2",
"@tailwindcss/vite": "^4.1.11",
"@tiptap/pm": "^3.0.9",
"@tiptap/starter-kit": "^3.0.9",
"@tiptap/vue-3": "^3.0.9",
"@vueuse/core": "^13.6.0",
"@vueuse/integrations": "^13.6.0",
"@vueuse/nuxt": "^13.6.0",
"@tailwindcss/vite": "^4.1.12",
"@tiptap/pm": "^3.3.0",
"@tiptap/starter-kit": "^3.3.0",
"@tiptap/vue-3": "^3.3.0",
"@vueuse/core": "^13.7.0",
"@vueuse/integrations": "^13.7.0",
"@vueuse/nuxt": "^13.7.0",
"better-sqlite3": "^12.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.537.0",
"lucide-vue-next": "^0.541.0",
"luxon": "^3.7.1",
"nuxt": "^4.0.3",
"pinia": "^3.0.3",
"radix-vue": "^1.9.17",
"reka-ui": "^2.4.1",
"shadcn-nuxt": "^2.2.0",
"sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.18",
"tw-animate-css": "^1.3.7",
"vue": "^3.5.19",
"vue-router": "^4.5.1",
"zod": "^4.0.15"
"zod": "^4.1.0"
},
"devDependencies": {
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/supabase": "^1.6.0",
"@nuxtjs/supabase": "^1.6.1",
"@nuxtjs/tailwindcss": "^6.14.0",
"@stylistic/eslint-plugin-js": "^4.4.1",
"@types/luxon": "^3.7.1",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"eslint": "^9.32.0",
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.4.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"sass": "^1.90.0",
"supabase": "^2.33.9",
"tailwindcss": "^4.1.11",
"supabase": "^2.34.3",
"tailwindcss": "^4.1.12",
"typescript": "^5.9.2",
"wrangler": "^4.28.1"
"wrangler": "^4.32.0"
}
}

3412
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff