1 Commits

Author SHA1 Message Date
Alexis
4ee412ee0c Added supabase and basic role management 2024-05-06 18:28:13 +02:00
366 changed files with 11137 additions and 24891 deletions

35
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,35 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
overrides: [
{
files: [
'**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/support/**/*.{js,ts,jsx,tsx}'
],
extends: ['plugin:cypress/recommended']
}
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
indent: ['error', 2, { SwitchCase: 1 }],
'prettier/prettier': [
'error',
{
endOfLine: 'auto'
}
],
'vue/multi-word-component-names': 'off'
}
}

44
.gitignore vendored
View File

@@ -1,28 +1,32 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs # Logs
logs logs
*.log *.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Misc node_modules
.DS_Store .DS_Store
.fleet dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea .idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# Local env files
.env .env
.env.*
!.env.example
# Supabase
.branches
.temp

3
.npmrc
View File

@@ -1,3 +0,0 @@
# https://pnpm.io/npmrc#shamefully-hoist
# Fixes: WARN Failed to resolve dependency: @supabase/gotrue-js, present in 'optimizeDeps.include'
shamefully-hoist=true

1
.nvmrc
View File

@@ -1 +0,0 @@
20.12.0

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@@ -1,5 +0,0 @@
{
"eslint.experimental.useFlatConfig": true,
"editor.indentSize": "tabSize",
"editor.tabSize": 2,
}

View File

@@ -1,75 +1,5 @@
# Nuxt 3 Minimal Starter # Leim Tools
A congregate of tools used to visualize TTRPG elements and help content creators (mostly me for now tbh) build their worlds.
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. ## Timeline
Visual helper for time-related data, such as events, eras and characters. Helpful to know how old a character is while something is happening in the world, or how far two events are separated in the timeline.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

57
app.vue
View File

@@ -1,57 +0,0 @@
<script setup lang="ts">
import { ConfigProvider } from "radix-vue"
useHead({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} — TTTools` : "TTTools";
},
meta: [
{ name: "charset", content: "UTF-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1.0" },
{ name: "author", content: "Alexis Pelé" },
{ name: "generator", content: "Nuxt" },
{ name: "msapplication-TileColor", content: "00aba9" },
{ name: "theme-color", content: "00aba9" },
{ name: "og:type", content: "tabletop-tools" },
{ name: "og:url", content: "ttt.alexcreates.fr" },
{ name: "robots", content: "noindex, nofollow"}
],
link: [
{ rel: "apple-touch-icon", sizes: "76x76", href: "/apple-touch-icon.png" },
{ rel: "icon", sizes: "32x32", type: "image/png", href: "/favicon-32x32.png" },
{ rel: "icon", sizes: "16x16", type: "image/png", href: "/favicon-16x16.png" },
{ rel: "manifest", href: "/site.webmanifest" },
{ rel: "mask-icon", href: "/safari-pinned-tab.svg", color: "#6595b4" },
]
})
const useIdFunction = () => useId()
</script>
<template>
<div class="h-screen">
<NuxtLoadingIndicator />
<NuxtLayout>
<ConfigProvider :use-id="useIdFunction">
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors">
<Sidebar />
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
<NuxtPage />
</div>
</div>
</ConfigProvider>
</NuxtLayout>
<ClientOnly>
<UiToaster />
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
.wrapper > * {
height: 100%;
}
</style>

View File

@@ -1,301 +0,0 @@
:root {
--event-red-foreground: var(--color-slate-900);
--event-red-background: var(--color-red-500);
--event-orange-foreground: var(--color-slate-900);
--event-orange-background: var(--color-orange-500);
--event-amber-foreground: var(--color-slate-900);
--event-amber-background: var(--color-amber-500);
--event-yellow-foreground: var(--color-slate-900);
--event-yellow-background: var(--color-yellow-500);
--event-lime-foreground: var(--color-slate-900);
--event-lime-background: var(--color-lime-500);
--event-green-foreground: var(--color-white);
--event-green-background: var(--color-green-600);
--event-emerald-foreground: var(--color-white);
--event-emerald-background: var(--color-emerald-600);
--event-teal-foreground: var(--color-white);
--event-teal-background: var(--color-teal-600);
--event-cyan-foreground: var(--color-white);
--event-cyan-background: var(--color-cyan-600);
--event-sky-foreground: var(--color-white);
--event-sky-background: var(--color-sky-600);
--event-blue-foreground: var(--color-white);
--event-blue-background: var(--color-blue-600);
--event-indigo-foreground: var(--color-white);
--event-indigo-background: var(--color-indigo-600);
--event-violet-foreground: var(--color-white);
--event-violet-background: var(--color-violet-600);
--event-purple-foreground: var(--color-white);
--event-purple-background: var(--color-purple-600);
--event-fuchsia-foreground: var(--color-white);
--event-fuchsia-background: var(--color-fuchsia-700);
--event-pink-foreground: var(--color-white);
--event-pink-background: var(--color-pink-700);
--event-rose-foreground: var(--color-white);
--event-rose-background: var(--color-pink-700);
--event-black-foreground: var(--color-white);
--event-black-background: var(--color-stone-500);
--event-white-foreground: var(--color-slate-900);
--event-white-background: var(--color-lime-100);
}
@theme inline {
--color-event-red-foreground: var(--event-red-foreground);
--color-event-red-background: var(--event-red-background);
--color-event-orange-foreground: var(--event-orange-foreground);
--color-event-orange-background: var(--event-orange-background);
--color-event-amber-foreground: var(--event-amber-foreground);
--color-event-amber-background: var(--event-amber-background);
--color-event-yellow-foreground: var(--event-yellow-foreground);
--color-event-yellow-background: var(--event-yellow-background);
--color-event-lime-foreground: var(--event-lime-foreground);
--color-event-lime-background: var(--event-lime-background);
--color-event-green-foreground: var(--event-green-foreground);
--color-event-green-background: var(--event-green-background);
--color-event-emerald-foreground: var(--event-emerald-foreground);
--color-event-emerald-background: var(--event-emerald-background);
--color-event-teal-foreground: var(--event-teal-foreground);
--color-event-teal-background: var(--event-teal-background);
--color-event-cyan-foreground: var(--event-cyan-foreground);
--color-event-cyan-background: var(--event-cyan-background);
--color-event-sky-foreground: var(--event-sky-foreground);
--color-event-sky-background: var(--event-sky-background);
--color-event-blue-foreground: var(--event-blue-foreground);
--color-event-blue-background: var(--event-blue-background);
--color-event-indigo-foreground: var(--event-indigo-foreground);
--color-event-indigo-background: var(--event-indigo-background);
--color-event-violet-foreground: var(--event-violet-foreground);
--color-event-violet-background: var(--event-violet-background);
--color-event-purple-foreground: var(--event-purple-foreground);
--color-event-purple-background: var(--event-purple-background);
--color-event-fuchsia-foreground: var(--event-fuchsia-foreground);
--color-event-fuchsia-background: var(--event-fuchsia-background);
--color-event-pink-foreground: var(--event-pink-foreground);
--color-event-pink-background: var(--event-pink-background);
--color-event-rose-foreground: var(--event-rose-foreground);
--color-event-rose-background: var(--event-rose-background);
--color-event-black-foreground: var(--event-black-foreground);
--color-event-black-background: var(--event-black-background);
--color-event-white-foreground: var(--event-white-foreground);
--color-event-white-background: var(--event-white-background);
}
.event-button,
.event-popover,
.event-callout {
--fg-color: var(--color-foreground);
--bg-color: var(--color-background);
transition-duration: var(--default-transition-duration);
transition-timing-function: var(--default-transition-timing-function);
}
.event-button {
color: var(--fg-color);
background-color: var(--bg-color);
text-underline-offset: .15rem;
transition-property: background-color, color;
}
.event-button:hover {
text-decoration: underline;
background-color: color-mix(in srgb, var(--bg-color) 70%, var(--color-background));
}
.event-button.is-hidden {
background-color: transparent;
color: var(--bg-color);
border: 1px dashed var(--bg-color);
transition-property: background-color, color, border-color;
}
.event-button.is-hidden:is(:hover, :focus-visible) {
background-color: color-mix(in srgb, var(--bg-color) 70%, var(--color-background));
color: var(--fg-color);
}
.event-popover {
background-color: color-mix(in srgb, var(--bg-color) 10%, var(--color-background));
border-color: color-mix(in srgb, var(--bg-color) 50%, var(--color-background));
transition-property: background-color, color;
hr {
border-color: var(--bg-color);
}
}
.event-callout {
position: relative;
background-color: color-mix(in srgb, var(--bg-color) 10%, var(--color-background));
border-top-right-radius: .2rem;
border-bottom-right-radius: .2rem;
transition-property: background-color, color;
hr {
border-color: var(--bg-color);
}
}
.event-callout::before {
display: block;
content: '';
position: absolute;
height: 100%;
inset-block: 0;
left: -0.5rem;
width: .5rem;
background-color: color-mix(in srgb, var(--bg-color) 70%, var(--color-background));
border-top-left-radius: .2rem;
border-bottom-left-radius: .2rem;
transition-property: background-color;
transition-duration: var(--default-transition-duration);
transition-timing-function: var(--default-transition-timing-function);
}
.event-callout:is(:hover, :focus-visible) {
background-color: color-mix(in srgb, var(--bg-color) 30%, var(--color-background));
}
.event-callout:is(:hover, :focus-visible)::before {
background-color: color-mix(in srgb, var(--bg-color) 90%, var(--color-background));
}
.event-callout.is-hidden {
border: 1px dashed var(--bg-color);
background-color: transparent;
}
.event-callout.is-hidden::before {
inset-block: -2px;
height: calc(100% + 3px);
background-color: color-mix(in srgb, var(--bg-color) 40%, var(--color-background));
}
.event-callout.is-hidden:is(:hover, :focus-visible) {
background-color: color-mix(in srgb, var(--bg-color) 5%, var(--color-background));
}
.event-callout.is-hidden:is(:hover, :focus-visible)::before {
background-color: color-mix(in srgb, var(--bg-color) 50%, var(--color-background));
}
.bgc {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.bgc::before {
content: "";
display: inline-block;
width: .75rem;
aspect-ratio: 1;
margin-right: 0.5em;
border-radius: .25rem;
background-color: var(--bg-color);
border: 1px solid transparent;
}
.element-red {
--fg-color: var(--color-event-red-foreground);
--bg-color: var(--color-event-red-background);
}
.element-orange {
--fg-color: var(--color-event-orange-foreground);
--bg-color: var(--color-event-orange-background);
}
.element-amber {
--fg-color: var(--color-event-amber-foreground);
--bg-color: var(--color-event-amber-background);
}
.element-yellow {
--fg-color: var(--color-event-yellow-foreground);
--bg-color: var(--color-event-yellow-background);
}
.element-lime {
--fg-color: var(--color-event-lime-foreground);
--bg-color: var(--color-event-lime-background);
}
.element-green {
--fg-color: var(--color-event-green-foreground);
--bg-color: var(--color-event-green-background);
}
.element-emerald {
--fg-color: var(--color-event-emerald-foreground);
--bg-color: var(--color-event-emerald-background);
}
.element-teal {
--fg-color: var(--color-event-teal-foreground);
--bg-color: var(--color-event-teal-background);
}
.element-cyan {
--fg-color: var(--color-event-cyan-foreground);
--bg-color: var(--color-event-cyan-background);
}
.element-sky {
--fg-color: var(--color-event-sky-foreground);
--bg-color: var(--color-event-sky-background);
}
.element-blue {
--fg-color: var(--color-event-blue-foreground);
--bg-color: var(--color-event-blue-background);
}
.element-indigo {
--fg-color: var(--color-event-indigo-foreground);
--bg-color: var(--color-event-indigo-background);
}
.element-violet {
--fg-color: var(--color-event-violet-foreground);
--bg-color: var(--color-event-violet-background);
}
.element-purple {
--fg-color: var(--color-event-purple-foreground);
--bg-color: var(--color-event-purple-background);
}
.element-fuchsia {
--fg-color: var(--color-event-fuchsia-foreground);
--bg-color: var(--color-event-fuchsia-background);
}
.element-pink {
--fg-color: var(--color-event-pink-foreground);
--bg-color: var(--color-event-pink-background);
}
.element-rose {
--fg-color: var(--color-event-rose-foreground);
--bg-color: var(--color-event-rose-background);
}
.element-black {
--fg-color: var(--color-event-black-foreground);
--bg-color: var(--color-event-black-background);
}
.element-white {
--fg-color: var(--color-event-white-foreground);
--bg-color: var(--color-event-white-background);
}

View File

@@ -1,47 +0,0 @@
@import 'tailwindcss';
@config '../tailwind.config.js';
@import "./theme.css" layer(theme);
@import "./_colors.css" layer(theme);
@import "./_colors.css" layer(utilities);
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border, currentcolor);
}
}
body {
color: var(--color-foreground);
background-color: var(--color-background);
}
.fade-enter-active,
.fade-leave-active {
transition: all .5s ease;
}
.fade-delay-enter-active,
.fade-delay-leave-active {
transition: all .5s ease 1s;
}
.fade-enter-from,
.fade-leave-to,
.fade-delay-enter-from,
.fade-delay-leave-to {
opacity: 0;
visibility: hidden;
}

View File

@@ -1,108 +0,0 @@
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--primary: hsl(245 58% 51%);
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(210 50% 95%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(156 72% 67%);
--success-muted: hsl(156 72% 90%);
--success-foreground: hsl(222.2 84% 4.9%);
--ring: hsl(222.2 84% 4.9%);
--radius: 0.5rem;
}
.dark {
--background: hsl(180 0 4.9%);
--foreground: hsl(210 40% 98%);
--muted: hsl(244 47% 20%);
--muted-foreground: hsl(215 20.2% 65.1%);
--popover: hsl(180 0 4.9%);
--popover-foreground: hsl(210 40% 98%);
--card: hsl(180 0 4.9%);
--card-foreground: hsl(210 40% 98%);
--border: hsl(0, 0%, 16%);
--input: hsl(244 47% 20%);
--primary: hsl(245 58% 51%);
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(244 47% 20%);
--secondary-foreground: hsl(210 40% 98%);
--accent: hsl(244 47% 20%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142 72% 29%);
--success-muted: hsl(142 72% 10%);
--success-foreground: hsl(210 40% 98%);
--ring: hsl(212.7 26.8% 83.9%);
--radius: 0.5rem;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-muted: var(--success-muted);
--color-success-foreground: var(--success-foreground);
--color-ring: var(--ring);
}

View File

@@ -2,14 +2,13 @@
"$schema": "https://shadcn-vue.com/schema.json", "$schema": "https://shadcn-vue.com/schema.json",
"style": "default", "style": "default",
"typescript": true, "typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "assets/main.css", "css": "src/assets/main.css",
"baseColor": "slate", "baseColor": "slate",
"cssVariables": true "cssVariables": true
}, },
"framework": "nuxt", "framework": "vite",
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils" "utils": "@/lib/utils"

View File

@@ -1,68 +0,0 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { computed, type Component, type ComputedRef } from "vue"
// import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import MonthlyLayout from "./state/monthly/Layout.vue"
import CenturyLayout from "./state/centennially/Layout.vue"
import DecadeLayout from "./state/decennially/Layout.vue"
import YearLayout from "./state/yearly/Layout.vue"
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
const { isReadOnly } = storeToRefs(useCalendar())
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) {
case "month":
return MonthlyLayout
case "year":
return YearLayout
case "decade":
return DecadeLayout
case "century":
default:
return CenturyLayout
}
})
onMounted(() => {
jumpToDate(selectedDate)
})
</script>
<template>
<div class="h-full w-full relative">
<div class="h-full grid grid-rows-[auto_1fr]">
<CalendarMenu />
<component :is="currentViewComponent" />
<LazyCalendarSearch />
<LazyCalendarDialogCategories v-if="!isReadOnly" />
<LazyCalendarDialogUpdateEvent v-if="!isReadOnly" />
<LazyCalendarDialogDeleteEvent v-if="!isReadOnly" />
</div>
</div>
</template>
<style lang="scss" scoped>
.screen-move, /* apply transition to moving elements */
.screen-enter-active,
.screen-leave-active {
transition: all .4s ease-out;
}
.screen-enter-from,
.screen-leave-to {
opacity: 0;
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.screen-leave-active {
position: absolute;
}
</style>

View File

@@ -1,23 +0,0 @@
<script lang="ts" setup>
import { PhTag } from "@phosphor-icons/vue"
const { toggleCategoriesModal } = useCalendar()
const { isReadOnly } = storeToRefs(useCalendar())
</script>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
<PhTag size="20" weight="light" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.seeCategories') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</template>

View File

@@ -1,35 +0,0 @@
<script lang="ts" setup>
import { storeToRefs } from "pinia"
import { computed } from "vue"
import { PhMapPin } from "@phosphor-icons/vue"
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
const isToday = computed(() => getDifferenceInDays(defaultDate, selectedDate.value) === 0)
</script>
<template>
<ClientOnly>
<div class="flex gap-2 items-center">
<h1 class="text-2xl font-bold flex items-center gap-1">
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
</h1>
<h2 v-if="!isToday" class="text-xl italic opacity-75">
{{ dateDifference }}
</h2>
</div>
<template #fallback>
<div class="flex items-center gap-1">
<UiSkeleton class="h-8 w-64" />
<UiSkeleton class="h-6 w-28" />
</div>
</template>
</ClientOnly>
</template>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { cn } from "@/lib/utils"
import { useCalendar } from "@/stores/CalendarStore"
import { PhCalendarBlank, PhCheckCircle, PhGear } from "@phosphor-icons/vue"
import { computed } from "vue"
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</script>
<template>
<UiDropdownMenu>
<UiDropdownMenuTrigger as-child>
<UiButton variant="secondary" size="icon">
<PhGear size="20" weight="fill" />
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="40" class="text-right">
<UiDropdownMenuArrow />
<UiDropdownMenuLabel>
{{ $t('entity.calendar.seeOptions') }}
</UiDropdownMenuLabel>
<UiDropdownMenuSub>
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">
{{ $t('ui.displayMode') }}
<PhCalendarBlank size="18" weight="fill" />
</UiDropdownMenuItem>
</UiDropdownMenuSubTrigger>
<UiDropdownMenuPortal>
<UiDropdownMenuSubContent>
<UiDropdownMenuItem
v-for="option in viewTypeOptions"
:key="option"
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': viewTypeTitle === getViewTypeTitle(option) })"
@click="setViewType(option)"
>
<PhCheckCircle v-if="viewTypeTitle === getViewTypeTitle(option)" size="20" weight="fill" />
{{ getViewTypeTitle(option) }}
</UiDropdownMenuItem>
</UiDropdownMenuSubContent>
</UiDropdownMenuPortal>
</UiDropdownMenuSub>
</UiDropdownMenuContent>
</UiDropdownMenu>
</template>

View File

@@ -1,102 +0,0 @@
<script lang="ts" setup>
import { PhArchive, PhCalendarDots, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
import { DateTime } from "luxon";
import type { Calendar } from "~/models/CalendarConfig";
const props = defineProps<{
calendar: Calendar,
gmId?: string,
showActions?: boolean,
}>()
const emit = defineEmits(["on-edit", "on-delete"])
const { locale } = useI18n();
const createdAt = DateTime.fromISO(props.calendar.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
const updatedAt = computed<string>(() => props.calendar.updatedAt ? DateTime.fromISO(props.calendar.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
const user = useSupabaseUser();
const isOwner = computed(() => user.value && props.gmId && user.value.id === props.gmId);
const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calendar.id}` : `/calendars/${props.calendar.shortId}`);
</script>
<template>
<UiCard
class="w-full h-full flex flex-col transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
:link="calendarLink"
:class="{
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !calendar.color,
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': calendar.color === 'red',
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': calendar.color === 'orange',
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': calendar.color === 'amber',
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': calendar.color === 'yellow',
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': calendar.color === 'lime',
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': calendar.color === 'green',
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': calendar.color === 'emerald',
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': calendar.color === 'teal',
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': calendar.color === 'cyan',
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': calendar.color === 'sky',
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': calendar.color === 'blue',
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': calendar.color === 'indigo',
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': calendar.color === 'violet',
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': calendar.color === 'purple',
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': calendar.color === 'fuchsia',
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': calendar.color === 'pink',
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': calendar.color === 'rose',
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': calendar.color === 'black',
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-200 dark:border-slate-700 dark:focus-within:outline-slate-100': calendar.color === 'white',
}"
>
<UiCardHeader class="gap-4">
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
<div v-if="calendar.state === 'published'" class="flex items-center gap-1 text-sm">
<PhFile size="20" weight="fill" />
<span>{{ $t('ui.contentState.published') }}</span>
</div>
<div v-if="calendar.state === 'draft'" class="flex items-center gap-1 text-sm">
<PhFileDashed size="20" weight="fill" />
<span>{{ $t('ui.contentState.draft') }}</span>
</div>
<div v-if="calendar.state === 'archived'" class="flex items-center gap-1 text-sm">
<PhArchive size="20" weight="fill" />
<span>{{ $t('ui.contentState.archived') }}</span>
</div>
</UiCardHeader>
<UiCardContent class="grow">
<p class="flex items-center gap-1">
<PhCalendarDots size="24" weight="fill" />
<span v-if="calendar.eventNb?.[0].count! > 1">{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
<span v-else>{{ $t("entity.calendar.hasXEvent", { count: calendar.eventNb?.[0].count }) }}</span>
</p>
<div
v-if="isOwner && showActions"
class="flex gap-1 absolute top-4 right-4 z-20"
>
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
<PhPencil size="16" />
</UiButton>
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
<PhTrash size="16" />
</UiButton>
</div>
</UiCardContent>
<UiCardFooter>
<ul class="grid gap-1 text-sm">
<li class="flex gap-1 items-center">
<PhFilePlus size="18" />
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
</li>
<li v-if="updatedAt" class="flex gap-1 items-center">
<PhPencilSimpleLine size="18" />
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
</li>
</ul>
</UiCardFooter>
</UiCard>
</template>

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { Category } from "~/models/Category";
defineProps<{
categories: Category[]
}>()
</script>
<template>
<ul>
<li v-for="category in categories" :key="category.id">
<CalendarCategoryListItem :category="category" />
</li>
</ul>
</template>

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { Category } from "~/models/Category";
defineProps<{
category: Category
}>()
</script>
<template>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ category.name }}
</span>
</div>
</template>

View File

@@ -1,72 +0,0 @@
<script setup lang="ts">
import type { Category } from "~/models/Category";
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
const { t } = useI18n()
const { categories } = defineProps<{
categories: Category[]
}>()
const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.localeCompare(b.name)))
const deleteDialogOpened = ref<boolean>(false)
function openDeleteDialog() {
deleteDialogOpened.value = true
}
function closeDeleteDialog() {
deleteDialogOpened.value = false
}
</script>
<template>
<div class="flex flex-col h-full">
<CalendarCategoryTableHeader />
<ScrollAreaRoot class="h-36 grow overflow-hidden ">
<ScrollAreaViewport class="w-full h-full pr-4" as-child>
<div class="last:border-0">
<CalendarCategoryTableRow
v-for="item in sortedCategories"
:key="item.id"
:category="item"
@on-delete-category="openDeleteDialog"
/>
</div>
</ScrollAreaViewport>
<ScrollAreaScrollbar
class="flex select-none touch-none p-0.5 bg-background transition-colors duration-100 ease-out hover:bg-foreground/5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5"
orientation="vertical"
>
<ScrollAreaThumb
class="flex-1 bg-foreground/20 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full"
/>
</ScrollAreaScrollbar>
</ScrollAreaRoot>
<CalendarCategoryTableFooter />
</div>
<UiDialog v-model:open="deleteDialogOpened">
<UiDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
@escape-key-down="closeDeleteDialog"
@focus-outside="closeDeleteDialog"
@interact-outside="closeDeleteDialog"
@pointer-down-outside="closeDeleteDialog"
>
<UiDialogTitle>
{{ $t('entity.category.deleteDialog.title') }}
</UiDialogTitle>
<UiDialogDescription>
{{ $t('entity.category.deleteDialog.subtitle') }}
</UiDialogDescription>
<CalendarFormDeleteCategory />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,130 +0,0 @@
<script lang="ts" setup>
import { PhPlus } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast"
import { ToastLifetime } from "~/components/ui/toast/use-toast"
import { cn } from "~/lib/utils"
const { toast } = useToast()
const { t } = useI18n()
type FooterMode = "add" | "view"
const currentMode = ref<FooterMode>("view")
const rowRef = ref<HTMLDivElement | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)
const { focused: inputFocused } = useFocus(inputRef)
/**
* Toggle view mode options
*/
type ToggleViewOptions = {
execution?: "now" | "nextTick"
}
/**
* Toggle view mode
* @param options.execution - When to execute the toggle. "now" or "nextTick"
*/
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
currentMode.value = "view"
if (options.execution === "now") {
inputFocused.value = false
} else {
nextTick(() => {
inputFocused.value = false
})
}
}
/**
* Toggle add mode
*/
function toggleAdd() {
currentMode.value = "add"
categorySkeleton.value = { name: "", color: "black" }
nextTick(() => {
inputFocused.value = true
})
}
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
const { addCategoryFromSkeleton } = useCategoryStore()
const { categorySkeleton } = storeToRefs(useCategoryStore())
/**
* Submit the update
*/
async function submitNew() {
if (!categorySkeleton.value) return
const newCategoryName = toRaw(categorySkeleton.value).name
const { error } = await tryCatch(addCategoryFromSkeleton())
if (error) {
toast({
title: t("entity.category.addedToast.titleError", { category: newCategoryName }),
variant: "destructive",
duration: ToastLifetime.LONG
})
return
}
toggleView({ execution: "now" })
toast({
title: t("entity.category.addedToast.title", { category: newCategoryName }),
variant: "success",
duration: ToastLifetime.SHORT
})
}
</script>
<template>
<div class="border-y-[1px] border-b-foreground/10 mr-4">
<form
ref="rowRef"
class="grid grid-cols-12 items-center gap-4 p-1 bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900"
@submit.prevent="submitNew"
>
<div v-if="currentMode === 'add'" class="col-span-1" />
<div
:class="cn({
'col-span-6': currentMode === 'view',
'col-span-5': currentMode === 'add'
})"
>
<template v-if="currentMode === 'view'">
<button
class="p-2 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
@click="toggleAdd"
>
<PhPlus size="18" />
</button>
</template>
<template v-if="currentMode === 'add' && categorySkeleton">
<input
ref="inputRef"
v-model="categorySkeleton.name"
type="text"
class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
>
</template>
</div>
<div class="col-span-4">
<template v-if="currentMode === 'add' && categorySkeleton">
<div class="-mx-2">
<InputColor
id="category-color"
v-model="categorySkeleton.color"
position="item-aligned"
theme="subtle"
/>
</div>
</template>
</div>
</form>
</div>
</template>

View File

@@ -1,19 +0,0 @@
<script setup lang="ts">
import { PhListBullets } from "@phosphor-icons/vue";
</script>
<template>
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-border font-bold mr-4">
<div class="col-span-1 flex items-end">
<PhListBullets size="20" />
</div>
<div class="col-span-5">
Nom
</div>
<div class="col-span-4">
Couleur
</div>
<div class="col-span-2" />
</header>
</template>

View File

@@ -1,187 +0,0 @@
<script setup lang="ts">
import { PhCheck, PhTrash } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast"
import { ToastLifetime } from "~/components/ui/toast/use-toast"
import { cn } from "~/lib/utils"
import type { Category } from "~/models/Category"
const { toast } = useToast()
const { t } = useI18n()
const { category } = defineProps<{
category: Category
}>()
const emit = defineEmits<{
(e: "on-delete-category", payload: Category): void
}>()
type RowMode = "edit" | "view"
const currentMode = ref<RowMode>("view")
const rowRef = ref<HTMLDivElement | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)
const { focused: inputFocused } = useFocus(inputRef)
const rowHovered = useElementHover(rowRef)
/**
* Toggle view mode options
*/
type ToggleViewOptions = {
execution?: "now" | "nextTick"
}
/**
* Toggle view mode
* @param options.execution - When to execute the toggle. "now" or "nextTick"
*/
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
currentMode.value = "view"
if (options.execution === "now") {
inputFocused.value = false
} else {
nextTick(() => {
inputFocused.value = false
})
}
}
/**
* Toggle edit mode
*/
function toggleEdit() {
currentMode.value = "edit"
categorySkeleton.value = structuredClone(toRaw(category))
nextTick(() => {
inputFocused.value = true
})
}
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
const { resetSkeleton, updateCategoryFromSkeleton } = useCategoryStore()
const { categorySkeleton } = storeToRefs(useCategoryStore())
onUnmounted(() => {
resetSkeleton()
})
/**
* Submit the update
*/
async function submitUpdate() {
const { error } = await tryCatch(updateCategoryFromSkeleton())
if (error) {
toast({
title: t("entity.category.updatedToast.titleError", { category: category.name }),
variant: "destructive",
duration: ToastLifetime.LONG
})
return
}
toggleView({ execution: "now" })
toast({
title: t("entity.category.updatedToast.title", { category: category.name }),
variant: "success",
duration: ToastLifetime.SHORT
})
}
function handleQueryDelete() {
categorySkeleton.value = structuredClone(toRaw(category))
emit("on-delete-category", category)
}
</script>
<template>
<div ref="rowRef" class="relative">
<form
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
:class="cn(
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
)"
@submit.prevent="submitUpdate"
>
<div class="col-span-1 pointer-events-none">
<span class="opacity-50 p-2">{{ category.id }}</span>
</div>
<div
class="col-span-5"
>
<template v-if="currentMode === 'view'">
<button
class="py-2 px-1 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
@click="toggleEdit"
>
{{ category.name }}
</button>
</template>
<template v-else-if="currentMode === 'edit' && categorySkeleton">
<input
ref="inputRef"
v-model="categorySkeleton.name"
type="text"
class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
>
</template>
</div>
<div class="col-span-4">
<template v-if="currentMode === 'view'">
<button
class="p-1 h-full w-full text-left text-sm cursor-pointer"
@click="toggleEdit"
>
<span
class="bgc"
:class="cn(`element-${category.color}`)"
>
{{ $t(`ui.colors.${category.color}`) }}
</span>
</button>
</template>
<template v-else-if="currentMode === 'edit' && categorySkeleton">
<div class="-mx-2">
<InputColor
id="category-color"
v-model="categorySkeleton.color"
position="item-aligned"
theme="subtle"
/>
</div>
</template>
</div>
</form>
<menu class="w-fit absolute top-1/2 -translate-y-1/2 right-2 flex items-center gap-2">
<li
v-if="currentMode === 'edit' && categorySkeleton"
>
<UiButton
variant="secondary"
size="icon"
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white"
:title="$t('ui.actions.edit')"
@click="submitUpdate"
>
<PhCheck size="14" weight="bold" />
</UiButton>
</li>
<li v-else-if="rowHovered">
<UiButton
variant="secondary"
size="icon"
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white"
:title="$t('ui.actions.delete')"
@click="handleQueryDelete"
>
<PhTrash size="14" weight="bold" />
</UiButton>
</li>
</menu>
</div>
</template>

View File

@@ -1,41 +0,0 @@
<script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue";
const { toggleCategoriesModal } = useCalendar()
const { categories, isCategoriesModalOpen } = storeToRefs(useCalendar())
function handleClosing() {
toggleCategoriesModal(false)
}
</script>
<template>
<UiAlertDialog :open="isCategoriesModalOpen">
<UiAlertDialogContent
class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl"
:disable-outside-pointer-events="true"
:trap-focus="true"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@close-auto-focus="(e) => e.preventDefault()"
>
<header>
<UiAlertDialogTitle>
<span class="text-2xl">
{{ $t('entity.category.manageDialog.title') }}
</span>
</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.category.manageDialog.subtitle') }}
</UiAlertDialogDescription>
</header>
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClosing">
<PhX size="20" />
</UiButton>
<CalendarCategoryTable :categories />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -1,47 +0,0 @@
<script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue";
import type { World } from "~/models/World";
defineProps<{
world: World,
modalState?: boolean
}>()
const calendarSkeletonName = ref<string>("")
function onChangedName(newName: string) {
calendarSkeletonName.value = newName
}
const emit = defineEmits(["on-close"])
function handleClose() {
emit("on-close")
setTimeout(() => calendarSkeletonName.value = "", 100)
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
<UiAlertDialogTitle>
<span class="text-2xl">
<strong class="font-bold">{{ world.name }}</strong>
<span class="opacity-30"> — </span>
<span v-if="calendarSkeletonName">
{{ calendarSkeletonName }}
</span>
<span v-else>
{{ $t('entity.calendar.createDialog.title') }}
</span>
</span>
</UiAlertDialogTitle>
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
<PhX size="20" />
</UiButton>
<CalendarFormCreate @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -1,62 +0,0 @@
<script lang="ts" setup>
import type { RPGDate } from "~/models/Date";
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
const { resetSkeleton } = useCalendar()
const popoverOpen = ref(false)
const props = defineProps<{
date?: RPGDate
btnClass?: string
}>()
/**
* Opens event creation's popover
*/
function openEventCreatePopover() {
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
if (operationInProgress.value) {
popoverOpen.value = false
return
}
resetSkeleton()
popoverOpen.value = true
// Set skeleton initial startDate if it's known
if (props.date) {
eventSkeleton.value.startDate = { ...props.date } // We need to clone it otherwise the props ends up mutating (?)
}
}
/**
* Prevents the modal from closing if's still loading
*
* @param e The closing event (can be keydown or click)
*/
function handleClosing() {
popoverOpen.value = false
resetSkeleton()
}
</script>
<template>
<UiPopover v-model:open="popoverOpen">
<UiPopoverTrigger as-child>
<button :class="btnClass" @click="openEventCreatePopover()" />
</UiPopoverTrigger>
<UiPopoverContent
:align="'center'"
:side="'right'"
:collision-padding="60"
:disable-outside-pointer-events="true"
:trap-focus="true"
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
@escape-key-down.prevent="handleClosing"
@pointer-down-outside.prevent="handleClosing"
>
<CalendarFormCreateEvent @event-created="handleClosing" />
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -1,112 +0,0 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { Calendar } from "~/models/CalendarConfig";
const { toast } = useToast()
const { t } = useI18n()
const props = defineProps<{
modalState: boolean,
calendar: Calendar | null
}>()
const isLoading = ref<boolean>(false)
const emit = defineEmits(["on-close"])
async function handleAction(): Promise<void> {
if (isLoading.value) return
if (!props.calendar) return
isLoading.value = true
try {
await $fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" })
emit("on-close")
toast({
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
variant: "success",
duration: ToastLifetime.SHORT
})
} catch (err) {
console.log(err)
if (err instanceof Error) {
toast({
title: err.message,
variant: "destructive"
})
}
} finally {
isLoading.value = false
}
}
/**
* Prevents the modal from closing if's still loading
*
* @param e The closing event (can be keydown or click)
*/
function handleClosing() {
if (!isLoading.value) {
emit("on-close")
}
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96"
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="handleClosing"
@close-auto-focus="(e) => e.preventDefault()"
>
<UiAlertDialogTitle>
{{ $t('entity.calendar.deleteDialog.title') }}
</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.calendar.deleteDialog.subtitle') }}
</UiAlertDialogDescription>
<form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-4">
<div class="text-red-500 ml-8">
<span class="text-sm">
<!-- {{ formErrors.message }} -->
</span>
</div>
</div>
<footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="handleClosing">
{{ $t('ui.action.back') }}
</UiButton>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton v-if="calendar" size="sm" variant="destructive" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('entity.deleteOne', { entity: calendar?.name }) }}
</UiButton>
</div>
</footer>
</form>
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -1,45 +0,0 @@
<script lang="ts" setup>
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
const isLoading = ref<boolean>(false)
// Watch the popover state
watch(isDeleteEventModalOpen, (hasOpened, _o) => {
if (hasOpened && lastActiveEvent.value) {
eventSkeleton.value = structuredClone(toRaw(lastActiveEvent.value))
}
})
/**
* Prevents the modal from closing if's still loading
*
* @param e The closing event (can be keydown or click)
*/
function handleClosing(e: Event): void {
if (isLoading.value) {
e.preventDefault()
}
}
</script>
<template>
<UiAlertDialog v-model:open="isDeleteEventModalOpen">
<UiAlertDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="handleClosing"
>
<UiAlertDialogTitle>{{ $t('entity.calendar.event.deleteDialog.title') }}</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.calendar.event.deleteDialog.subtitle') }}
</UiAlertDialogDescription>
<CalendarFormDeleteEvent />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -1,41 +0,0 @@
<script setup lang="ts">
import { PhPlus } from "@phosphor-icons/vue";
const isDialogOpen = ref<boolean>(false);
const { resetSkeleton } = useCalendar();
// Toggles the dialog
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
/**
* Prevents the modal from closing if's still loading
*/
function handleClosing() {
setTimeout(() => resetSkeleton(), 100)
}
</script>
<template>
<UiButton @click="toggleDialog">
<PhPlus size="18" weight="bold" />
<strong class="font-semibold">
{{ $t("entity.calendar.event.newEvent") }}
</strong>
</UiButton>
<UiDialog v-model:open="isDialogOpen">
<UiDialogContent
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
@escape-key-down.prevent="handleClosing"
@pointer-down-outside.prevent="handleClosing"
>
<UiDialogTitle>
{{ $t("entity.calendar.event.addSingle") }}
</UiDialogTitle>
<CalendarFormCreateEvent @event-created="toggleDialog" />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,49 +0,0 @@
<script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig";
import type { World } from "~/models/World";
const props = defineProps<{
calendar: Calendar | null,
world: World | null,
modalState?: boolean
}>()
const calendarSkeletonName = ref<string>(props.calendar?.name ?? "")
function onChangedName(newName: string) {
calendarSkeletonName.value = newName
}
const emit = defineEmits(["on-close"])
function handleClose() {
emit("on-close")
setTimeout(() => calendarSkeletonName.value = "", 100)
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
<UiAlertDialogTitle>
<span class="text-2xl">
<strong class="font-bold">{{ world?.name }}</strong>
<span class="opacity-30"> — </span>
<span v-if="calendarSkeletonName">
{{ calendarSkeletonName }}
</span>
<span v-else>
{{ $t('entity.calendar.updateDialog.title') }}
</span>
</span>
</UiAlertDialogTitle>
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
<PhX size="20" />
</UiButton>
<CalendarFormUpdate v-if="calendar" :world="world" :calendar="calendar" @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -1,47 +0,0 @@
<script lang="ts" setup>
const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
const { resetSkeleton } = useCalendar();
// Watch the popover state
watch(isEditEventModalOpen, (hasOpened, _o) => {
if (hasOpened && lastActiveEvent.value) {
eventSkeleton.value = structuredClone(toRaw(lastActiveEvent.value))
}
})
/**
* Prevents the modal from closing if's still loading
*
* @param e The closing event (can be keydown or click)
*/
function handleClosing() {
isEditEventModalOpen.value = false
setTimeout(() => resetSkeleton(), 100)
}
</script>
<template>
<UiDialog v-model:open="isEditEventModalOpen">
<UiDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="pl-3 min-w-96 border-border"
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="(e) => e.preventDefault()"
>
<header class="pl-8 grid gap-y-2">
<UiDialogTitle>
{{ $t('entity.calendar.event.editDialog.title') }}
</UiDialogTitle>
<UiDialogDescription>
{{ $t('entity.calendar.event.editDialog.subtitle') }}
</UiDialogDescription>
</header>
<CalendarFormUpdateEvent @event-updated="handleClosing" />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,207 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent"
import { useCalendar } from "@/stores/CalendarStore"
import {
PhHourglassMedium,
PhAlarm,
PhHourglassHigh,
PhHourglassLow,
PhMapPinArea,
PhDotsThreeOutlineVertical,
PhEye
} from "@phosphor-icons/vue"
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
const props = defineProps<{
event: CalendarEvent
spansMultipleDays: boolean
isStartEvent?: boolean
isEndEvent?: boolean
}>()
// Ref for the popover
const eventDetails = ref<HTMLElement>()
const emit = defineEmits(["query:close-popover"])
const dateDifference = computed<string>(() => getRelativeString(defaultDate, props.event.startDate))
const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
function handleJumpToDate(date: RPGDate) {
jumpToDate(date)
emit("query:close-popover")
}
/**
* Edit event
*/
const commandMenuOpened = ref(false)
function deployEditModal() {
lastActiveEvent.value = structuredClone(toRaw(props.event))
revealEditEventModal()
commandMenuOpened.value = false
emit("query:close-popover")
}
/**
* Confirm event deletion
*/
function deployDeleteModal() {
lastActiveEvent.value = structuredClone(toRaw(props.event))
revealDeleteEventModal()
commandMenuOpened.value = false
emit("query:close-popover")
}
</script>
<template>
<UiPopoverContent
class="w-96 event-popover"
:align="'center'"
:align-offset="50"
:side="'left'"
:collision-padding="60"
:hide-when-detached="true"
:class="cn(
event.category ? `element-${event.category.color}` : '',
)"
@focus-outside.prevent
>
<div ref="eventDetails" class="grid gap-1">
<header class="pr-12">
<div class="mb-2 max-h-24 overflow-y-auto text-lg font-semibold">
{{ event.title }}
</div>
<div class="mb-1">
<template v-if="!event.endDate">
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
</template>
<template v-else>
<p class="font-semibold">
{{
$t('entity.calendar.date.fromTo',
{
startDate: getFormattedDateTitle(event.startDate, true),
endDate: getFormattedDateTitle(event.endDate, true)
}
)
}}
</p>
</template>
</div>
</header>
<div class="mb-1 space-y-1">
<template v-if="event.location">
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhMapPinArea size="16" weight="fill" />
</div>
<p class="max-h-12 overflow-y-auto">{{ event.location }}</p>
</div>
</template>
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhAlarm size="16" weight="fill" />
</div>
<p>{{ dateDifference }}</p>
</div>
<template v-if="dateDuration">
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhHourglassMedium size="16" weight="fill" />
</div>
<p>{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}</p>
</div>
</template>
</div>
<template v-if="event.category || event.secondaryCategories">
<ul class="flex gap-1">
<li v-if="event.category">
<UiBadge class="mix-blend-luminosity font-bold bg-gray-400 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }}
</UiBadge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ cat.name }}
</UiBadge>
</li>
</ul>
</template>
<template v-if="event.description">
<hr class="border-border mt-2" >
<div class="max-h-48 overflow-y-auto mt-2 text-sm text-slate-600 dark:text-slate-300">
{{ event.description }}
</div>
</template>
</div>
<menu v-if="!isReadOnly" class="absolute top-4 right-4" :class="cn({ 'top-6': event.hidden })">
<UiPopover v-model:open="commandMenuOpened">
<UiPopoverTrigger as-child>
<UiButton size="icon" variant="outline" class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600">
<PhDotsThreeOutlineVertical size="20" weight="fill" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent class="w-fit p-0" align="start" side="right" :collision-padding="20" >
<UiCommand>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem value="edit-event" @select="deployEditModal"> {{ $t('ui.action.edit') }} </UiCommandItem>
<UiCommandItem value="delete-event" @select="deployDeleteModal"> {{ $t('ui.action.delete') }} </UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</menu>
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child>
<button
class="flex gap-1"
title="Naviguer au début"
@click="handleJumpToDate(event.startDate!)"
>
<PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }}
</button>
</UiBadge>
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child title="Naviguer à la fin">
<button
class="flex gap-1"
title="Naviguer à la fin"
@click="handleJumpToDate(event.endDate!)"
>
<PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }}
</button>
</UiBadge>
</nav>
<UiTooltipProvider v-if="event.hidden" :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiBadge class="absolute -top-2 right-2 flex gap-1">
<PhEye size="16" weight="fill" /> {{ $t('entity.calendar.event.isHidden') }}
</UiBadge>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.event.hiddenTooltip') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</UiPopoverContent>
</template>

View File

@@ -1,86 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "~/models/CalendarEvent"
const props = defineProps<{
event: CalendarEvent
tileDate: RPGDate
}>()
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent } = storeToRefs(useCalendar())
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
const isEndEvent = computed(() => spansMultipleDays.value && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate))
const titleCharLimit = 50;
const eventTitle = computed<string>(() => props.event.title.length <= titleCharLimit ? props.event.title : `${props.event.title.slice(0, titleCharLimit)}`)
// Popover code
const isPopoverDetailsOpen = ref<boolean>(false)
function handleDoubleClick() {
isPopoverDetailsOpen.value = false
lastActiveEvent.value = structuredClone(toRaw(props.event))
revealEditEventModal()
}
function handleDelete() {
isPopoverDetailsOpen.value = false
lastActiveEvent.value = structuredClone(toRaw(props.event))
revealDeleteEventModal()
}
function handleClosePopover() {
isPopoverDetailsOpen.value = false
}
onMounted(() => {
// Listen for keydown events
window.addEventListener("keydown", (e: KeyboardEvent) => {
// If the popover isn't opened, this is not the event we're trying to delete, so return
if (!isPopoverDetailsOpen.value) return
// If the key isn't the delete one, return
if (e.key !== "Delete") return
handleDelete()
})
})
</script>
<template>
<UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child>
<button
class="event-button text-xs px-2 py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer"
:class="
cn(
`element-${event.category?.color}`,
{
'is-hidden': event.hidden,
'rounded-r-none': isStartEvent,
'rounded-l-none': isEndEvent,
}
)
"
@dblclick="handleDoubleClick"
@keydown.delete="handleDelete"
>
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
{{ eventTitle }}
</div>
</button>
</UiPopoverTrigger>
<LazyCalendarEventDetails
:event
:spans-multiple-days
:is-start-event
:is-end-event
@query:close-popover="handleClosePopover"
/>
</UiPopover>
</template>

View File

@@ -1,151 +0,0 @@
<script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig";
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft", color: "white" }
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
onMounted(() => {
calendarSkeleton.value = { ...defaultSkeleton }
})
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
/**
* === Current date ===
*/
// If the months data change, just reset today's month
// This is a failsafe mainly because of 1) month positions and 2) month names
watch(calendarSkeleton.value.months, () => {
calendarSkeleton.value.today.month = 0
}, { deep: true })
/**
* === Form Validation ===
*/
/** Whether the skeleton has valid month data */
const validSkeletonMonths = computed(() => calendarSkeleton.value.months.length > 0)
/** Whether the skeleton has a valid name */
const validSkeletonGeneral = computed(() => calendarSkeleton.value.name)
/** Whether all the data checks above are a-ok */
const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeletonMonths.value)
/** Send the data to the store for validation */
const isCreatingCalendar = ref<boolean>(false)
async function handleSubmit() {
try {
isCreatingCalendar.value = true
await $fetch("/api/calendars/create", { method: "POST", body: { ...calendarSkeleton.value, worldId: 1 } })
emit("on-close")
} catch (err) {
console.log(err)
} finally {
isCreatingCalendar.value = false
}
}
/**
* === Watch for name changes to display above ===
*/
const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars
(e: "on-changed-name", calendarName: string): void
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>()
/** Hook to emit a debounced event for the changed skeleton name */
const handleNameChange = useDebounceFn(() => {
emit("on-changed-name", calendarSkeleton.value.name)
}, 400)
function handleFormCancel() {
emit("on-close")
}
</script>
<template>
<template v-if="calendarSkeleton">
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<UiTabs v-model:model-value="activeTab">
<UiTabsList class="grid w-full grid-cols-3 mb-4">
<UiTabsTrigger value="global" class="font-bold">
<div class="flex items-center gap-1">
<PhWrench size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div>
</UiTabsTrigger>
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
@input="handleNameChange"
>
<div class="-mx-1 px-1 grid gap-3">
<UiLabel for="new-calendar-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-calendar-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div>
</UiTabsContent>
<UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
</UiTabsContent>
<UiTabsContent value="today">
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
</UiTabsContent>
</UiTabs>
<footer class="flex justify-end gap-2 mt-6">
<UiButton type="button" variant="destructive" @click="handleFormCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
<UiButton type="submit" :disabled="!validSkeleton || isCreatingCalendar">
<Transition name="fade">
<PhCircleNotch v-if="isCreatingCalendar" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</footer>
</form>
</template>
</template>

View File

@@ -1,195 +0,0 @@
<script lang="ts" setup>
import type { RPGDate } from "~/models/Date";
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue"
const { t } = useI18n()
const emit = defineEmits(["event-created"])
const props = defineProps<{
date?: RPGDate
btnClass?: string
}>()
const { eventSkeleton } = storeToRefs(useCalendar())
const { submitSkeleton, cancelLatestRequest } = useCalendar()
const newEventTitle = shallowRef<HTMLInputElement>()
useFocus(newEventTitle, { initialValue: true })
const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
async function handleSubmit() {
// Prevent form submission if already loading
if (isLoading.value) return
isLoading.value = true
try {
await submitSkeleton()
emit("event-created")
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false
}
}
/**
* Click on the cancel button
*
* Must cancel the abortController in the store, and stop the loading
*/
function handleCancel() {
cancelLatestRequest()
isLoading.value = false
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-y-3">
<div class="col-span-2 pl-8">
<input
id="new-event-title"
ref="newEventTitle"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
:placeholder="$t('entity.calendar.event.title')"
:maxlength="120"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }}
</div>
</div>
<div class="col-span-2 my-2 pl-8">
<textarea
id="new-event-description"
v-model="eventSkeleton.description"
name="new-event-description"
:placeholder="$t('entity.addDescription')"
:maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
/>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate"
:placeholder="$t('entity.calendar.date.start')"
:initial-date="props.date"
:required="true"
/>
<span></span>
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate"
:placeholder="$t('entity.calendar.date.end')"
:initial-date="props.date"
/>
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
:maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }}
</div>
</div>
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
<PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden">
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel>
</div>
</div>
</div>
<div class="text-red-500 pl-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton size="sm" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</div>
</div>
</form>
</template>

View File

@@ -1,85 +0,0 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast";
const { deleteCategoryFromSkeleton, cancelLatestRequest } = useCategoryStore()
const { isDeletingCategory, categorySkeleton } = storeToRefs(useCategoryStore())
const { toast } = useToast()
const { t } = useI18n()
const isLoading = ref<boolean>(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
async function handleAction(): Promise<void> {
if (isLoading.value && !categorySkeleton.value) return
isLoading.value = true
const categoryName = categorySkeleton.value?.name
try {
await deleteCategoryFromSkeleton()
toast({
title: t("entity.category.deletedToast.title", { category: categoryName }),
variant: "success",
duration: ToastLifetime.MEDIUM
})
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false
}
}
/**
* Click on the cancel button
*
* Must cancel the abortController in the store, and stop the loading
*/
function handleCancel(): void {
cancelLatestRequest()
isLoading.value = false
}
</script>
<template>
<form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-4">
<div class="text-red-500 ml-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
</div>
<footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="() => isDeletingCategory = false">
{{ $t('ui.action.back') }}
</UiButton>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton size="sm" variant="destructive" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('ui.action.delete') }}
</UiButton>
</div>
</footer>
</form>
</template>

View File

@@ -1,88 +0,0 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast";
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
const { isDeleteEventModalOpen, eventSkeleton } = storeToRefs(useCalendar())
const { toast } = useToast()
const { t } = useI18n()
const isLoading = ref<boolean>(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
async function handleAction(): Promise<void> {
if (isLoading.value) return
isLoading.value = true
const eventTitle = eventSkeleton.value.title
try {
await deleteEventFromSkeleton()
isDeleteEventModalOpen.value = false
resetSkeleton()
toast({
title: t("entity.calendar.event.deletedToast.title", { event: eventTitle }),
variant: "success",
duration: ToastLifetime.MEDIUM
})
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false
}
}
/**
* Click on the cancel button
*
* Must cancel the abortController in the store, and stop the loading
*/
function handleCancel(): void {
cancelLatestRequest()
isLoading.value = false
}
</script>
<template>
<form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-4">
<div class="text-red-500 ml-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
</div>
<footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="() => isDeleteEventModalOpen = false">
{{ $t('ui.action.back') }}
</UiButton>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton size="sm" variant="destructive" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('ui.action.delete') }}
</UiButton>
</div>
</footer>
</form>
</template>

View File

@@ -1,147 +0,0 @@
<script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig";
import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
import type { World } from "~/models/World";
const props = defineProps<{
calendar: Calendar | null,
world: World | null,
}>()
const calendarSkeleton = ref<Calendar>({ ...props.calendar } as Calendar)
onMounted(() => {
calendarSkeleton.value = { ...props.calendar } as Calendar
})
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
/**
* === Form Validation ===
*/
/** Whether the skeleton has valid month data */
const validSkeletonMonths = computed(() => calendarSkeleton.value.months.length > 0)
/** Whether the skeleton has a valid name */
const validSkeletonGeneral = computed(() => calendarSkeleton.value.name)
/** Whether all the data checks above are a-ok */
const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeletonMonths.value)
/** Send the data to the store for validation */
const isUpdatingCalendar = ref<boolean>(false)
async function handleSubmit() {
try {
isUpdatingCalendar.value = true
await $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.world?.id } })
emit("on-close")
} catch (err) {
console.log(err)
} finally {
isUpdatingCalendar.value = false
}
}
/**
* === Watch for name changes to display above ===
*/
const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars
(e: "on-changed-name", calendarName: string): void
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>()
/** Hook to emit a debounced event for the changed skeleton name */
const handleNameChange = useDebounceFn(() => {
emit("on-changed-name", calendarSkeleton.value.name)
}, 400)
function handleFormCancel() {
emit("on-close")
}
</script>
<template>
<template v-if="calendarSkeleton">
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<UiTabs v-model:model-value="activeTab">
<UiTabsList class="grid w-full grid-cols-1 mb-4">
<UiTabsTrigger value="global" class="font-bold">
<div class="flex items-center gap-1">
<PhWrench size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div>
</UiTabsTrigger>
<!-- <UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div>
</UiTabsTrigger> -->
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
@input="handleNameChange"
>
<div class="-mx-1 px-1 grid gap-3">
<UiLabel for="new-calendar-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-calendar-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div>
</UiTabsContent>
<!-- <UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
</UiTabsContent>
<UiTabsContent value="today">
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
</UiTabsContent> -->
</UiTabs>
<footer class="flex justify-end gap-2 mt-6">
<UiButton type="button" variant="destructive" @click="handleFormCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
<UiButton type="submit" :disabled="!validSkeleton || isUpdatingCalendar">
<Transition name="fade">
<PhCircleNotch v-if="isUpdatingCalendar" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</footer>
</form>
</template>
</template>

View File

@@ -1,214 +0,0 @@
<script lang="ts" setup>
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { APIError } from "~/models/Errors";
const emit = defineEmits(["event-updated"])
const { resetSkeleton, updateEventFromSkeleton, cancelLatestRequest } = useCalendar()
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
const { toast } = useToast()
const { t } = useI18n()
const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
async function handleAction() {
if (isLoading.value) return
isLoading.value = true
try {
await updateEventFromSkeleton()
emit("event-updated")
toast({
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
variant: "success",
duration: ToastLifetime.SHORT
})
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const apiError = (err as any).data as APIError
apiError.data.errors.forEach((error) => {
toast({
title: t("entity.calendar.event.editErrors.toastTitle"),
variant: "destructive",
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
duration: ToastLifetime.MEDIUM,
})
})
} finally {
resetSkeleton()
isLoading.value = false
}
}
/**
* Click on the cancel button
*
* Must cancel the abortController in the store, and stop the loading
*/
function handleCancel() {
cancelLatestRequest()
isLoading.value = false
}
</script>
<template>
<form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-3">
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhPencilSimpleLine size="20" weight="fill" />
<div class="grow">
<input
id="new-event-title"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
:placeholder="$t('entity.calendar.event.title')"
:minlength="3"
:maxlength="120"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }}
</div>
</div>
</div>
</div>
<div class="col-span-2 my-2 ml-8">
<textarea
id="new-event-description"
v-model="eventSkeleton.description"
name="new-event-description"
:placeholder="$t('entity.addDescription')"
:maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
/>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate"
:placeholder="$t('entity.calendar.date.start')"
:initial-date="lastActiveEvent?.startDate"
:required="true"
/>
<span></span>
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate"
:placeholder="$t('entity.calendar.date.end')"
:initial-date="lastActiveEvent?.endDate"
/>
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div>
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
</div>
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
:minlength="3"
:maxlength="160"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }}
</div>
</div>
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
<PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden">
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel>
</div>
</div>
</div>
<div class="text-red-500 ml-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
</div>
<footer class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton size="sm" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</footer>
</form>
</template>

View File

@@ -1,84 +0,0 @@
<script lang="ts" setup>
import type { Category } from "~/models/Category";
import { PhCaretDown, PhCheck } from "@phosphor-icons/vue";
const isPopoverOpen = ref<boolean>(false)
const props = defineProps<{
placeholder?: string
}>()
const model = defineModel<Category[]>({ default: [] })
const modelBuffer = ref<Category[]>([])
watch(modelBuffer.value, () => {
model.value = [ ...modelBuffer.value ]
})
const { categories: availableCategories } = useCalendar()
const searchTerm = ref<string>("")
const filteredCategories = computed(() =>
searchTerm.value === ""
? availableCategories
: availableCategories.filter((category) => {
return category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
})
)
</script>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopoverTrigger as-child>
<UiButton
variant="outline"
role="combobox"
class="relative w-full max-w-full h-fit justify-between"
>
<template v-if="!model.length">
{{ props.placeholder }}
</template>
<template v-else>
<ul class="flex flex-wrap gap-1">
<li v-for="category in model" :key="`selected-cat-${category.id}`">
<UiBadge class="lowercase" variant="secondary">
{{ category.name }}
</UiBadge>
</li>
</ul>
</template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent
align="start"
side="bottom"
:collision-padding="50"
class="w-fit h-[33vh] p-0"
>
<UiCommand v-model="modelBuffer" v-model:searchTerm="searchTerm" :multiple="true">
<UiCommandInput :placeholder="$t('entity.category.search')" />
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem
v-for="category in filteredCategories"
:key="category.id"
:value="category"
class="cursor-pointer flex justify-between items-center"
>
<span>
{{ category.name }}
</span>
<PhCheck v-if="model.find(cat => cat.id === category.id)" />
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -1,73 +0,0 @@
<script lang="ts" setup>
import { PhCaretDown } from "@phosphor-icons/vue";
import type { Category } from "~/models/Category";
const isPopoverOpen = ref<boolean>(false)
const props = defineProps<{
placeholder?: string
}>()
const model = defineModel<Category>()
const { categories: availableCategories } = useCalendar()
const searchTerm = ref<string>("")
function handleCatSelect() {
isPopoverOpen.value = false
}
const filteredCategories = computed(() =>
searchTerm.value === ""
? availableCategories
: availableCategories.filter((category) => {
return category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
})
)
</script>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopoverTrigger as-child>
<UiButton
variant="outline"
role="combobox"
class="w-full max-w-full justify-between"
>
<template v-if="!model">
{{ props.placeholder }}
</template>
<template v-else>
{{ model.name }}
</template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent
align="start"
side="bottom"
:collision-padding="50"
class="w-fit h-[33vh] p-0"
>
<UiCommand v-model="model" v-model:searchTerm="searchTerm">
<UiCommandInput :placeholder="$t('entity.category.search')" />
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem
v-for="category in filteredCategories"
:key="category.id"
:value="category"
class="cursor-pointer"
@select="handleCatSelect"
>
{{ category.name }}
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -1,172 +0,0 @@
<script lang="ts" setup>
import { cn } from "~/lib/utils";
import { useSortable } from "@vueuse/integrations/useSortable";
import type { CalendarMonth } from "~/models/CalendarMonth";
import { PhList, PhPlus, PhTrash } from "@phosphor-icons/vue";
const model = defineModel<CalendarMonth[]>({ required: true })
/**
* Input value for new month's name
*/
const monthName: Ref<string | undefined> = ref<string>()
const monthNameRef = ref<HTMLInputElement>()
const { focused: monthNameFocused } = useFocus(monthNameRef)
const validMonthNameDatatypes = ["string"]
const monthNameIsTaken = computed(() => model.value.find(m => m.name === monthName.value))
const validMonthName = computed(() => validMonthNameDatatypes.includes(typeof monthName.value) && !monthNameIsTaken.value)
/**
* Input value for new month's number of days
*/
const monthDays: Ref<number | undefined> = ref<number>()
const monthDaysRef = ref<HTMLInputElement>()
const validMonthDaysDatatypes = ["number"]
const validMonthDays = computed(() => validMonthDaysDatatypes.includes(typeof monthDays.value) && monthDays.value && monthDays.value >= 12)
const validNewMonth = computed(() => validMonthDays.value && validMonthName.value)
/**
* Add current month input data to the model list
*/
function addMonthToModel(): void {
if (!monthDays.value || !monthName.value) return
// Create month object to add
const monthToInsert: CalendarMonth = {
name: monthName.value,
days: monthDays.value,
position: model.value.length
}
model.value.push(monthToInsert)
// Reset form state
monthName.value = ""
monthNameFocused.value = true
}
/**
* Remove a specific month from the model
*
* @param index Index position of the month in the model
*/
function removeMonthFromModel(index: number) {
if (isNaN(index)) return
model.value.splice(index, 1)
}
// Sortable setup
const monthSortableList = ref<HTMLElement | null>(null)
useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle" })
</script>
<template>
<div class="grid md:grid-cols-12 gap-4 items-center">
<div class="md:col-start-2 md:col-span-5">
<UiInput
id="new-month-name"
ref="monthNameRef"
v-model="monthName"
type="text"
name="newMonthName"
:placeholder="$t('entity.calendar.months.inputName')"
:class="cn({ 'border-red-600': monthNameIsTaken })"
/>
</div>
<div class="md:col-span-5">
<UiInput
id="new-month-days"
ref="monthDaysRef"
v-model="monthDays"
type="number"
name="newMonthName"
:placeholder="$t('entity.calendar.months.daysNb')"
min="0"
step="1"
class="invalid:border-red-600"
/>
</div>
<div class="md:col-span-1">
<UiButton size="icon" class="rounded-full h-8 w-8" :disabled="!validNewMonth" @click.prevent="addMonthToModel">
<PhPlus size="17"/>
</UiButton>
</div>
<div class="md:col-span-full">
<div
class="border-[1px] border-border p-4 rounded-sm max-h-80 overflow-y-auto"
:class="model.length ? 'md:grid md:grid-cols-12 md:gap-4 md:items-center' : ''"
>
<div v-if="model.length" class="hidden md:block col-span-1">
<ul class="grid gap-y-4 justify-center">
<li v-for="(m, i) in model" :key="`num-${m.name}`">
<UiButton size="icon" variant="secondary" class="h-8 w-8 rounded-full">
<span class="font-bold text-sm">{{ i + 1 }}</span>
</UiButton>
</li>
</ul>
</div>
<div class="md:col-span-11">
<ul ref="monthSortableList" class="grid gap-y-2" :class="model.length ? 'visible' : 'absolute invisible'">
<template v-if="model.length">
<li v-for="(m, i) in model" :key="m.name" class="grid md:grid-cols-12 gap-4 md:items-center text-slate-900 bg-slate-200 rounded-md">
<div class="md:col-span-1 text-right duration-200 ease-out transition transform origin-top-right">
<UiButton type="button" variant="ghost" size="icon" class="handle rounded-full h-8 w-8">
<PhList size="17" />
</UiButton>
</div>
<div class="md:col-span-3">
<div class="font-bold md:pl-2">
{{ m.name }}
</div>
</div>
<div class="md:col-span-2">
<UiInput
:id="`month-days-n${i}`"
v-model="m.days"
class="bg-transparent border-none"
type="number"
:name="`monthDays-n${i}`"
:placeholder="$t('entity.calendar.months.daysNb')"
min="0"
/>
</div>
<div class="md:col-span-1">
{{ $t('entity.calendar.months.daysMaybePlural') }}
</div>
<div class="md:col-start-12">
<UiTooltipProvider>
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton type="button" variant="ghost" size="icon" class="rounded-full h-8 w-8" @click="removeMonthFromModel(i)">
<PhTrash size="17" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.months.deleteOne', { month: m.name }) }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</li>
</template>
</ul>
</div>
<template v-if="!model.length">
<p class="col-span-12 text-lg text-center italic opacity-50">
{{ $t('entity.calendar.months.none') }}
</p>
</template>
</div>
</div>
</div>
</template>

View File

@@ -1,169 +0,0 @@
<script lang="ts" setup>
import type { RPGDate } from "~/models/Date";
import {
PhXCircle
} from "@phosphor-icons/vue"
const model = defineModel<RPGDate | null>()
const id = useId()
const props = defineProps<{
initialDate?: RPGDate
placeholder?: string
required?: boolean
}>()
const { getFormattedDateTitle, getMonthName, defaultDate } = useCalendar()
const { sortedMonths } = storeToRefs(useCalendar())
const popoverOpened = ref<boolean>(false)
const inputPlaceholder = computed<string>(() => {
if (model.value) {
return getFormattedDateTitle(model.value, true)
} else if (props.placeholder) {
return props.placeholder
}
return ""
})
const initialDateValue = props.initialDate ? { ...props.initialDate } : null
const monthTitle = computed<string>(() => {
if (model.value) {
return getMonthName(model.value.month)
}
return ""
})
const hasValue = computed<boolean>(() => !!model.value)
const monthData = computed(() => {
if (model.value) {
return sortedMonths.value[model.value.month]
}
return null
})
function setModelDay(day: number) {
if (model.value) {
model.value.day = day
}
}
function handleMonthChange(e: string) {
const monthId = Number(e)
if (model.value) {
model.value.month = monthId
}
}
/**
* Handle click if the date is null
*/
function handleNullClick() {
// Early return
if (model.value) return
if (!model.value) {
if (props.initialDate) {
model.value = initialDateValue
} else {
model.value = { ...defaultDate }
}
}
}
function handleValueReset() {
// Early return
if (!model.value) return
model.value = null
}
</script>
<template>
<UiPopover v-model:open="popoverOpened">
<UiPopoverTrigger>
<div class="flex">
<UiButton
size='sm'
variant="outline"
:class="{
'text-slate-500': !hasValue,
'rounded-tr-none rounded-br-none': model
}"
@click.prevent="handleNullClick"
>
{{ inputPlaceholder }}
</UiButton>
<UiButton
v-if="model && !required"
size='sm'
variant="outline"
class="rounded-tl-none rounded-bl-none px-2"
@click.prevent="handleValueReset"
>
<PhXCircle size="14" />
</UiButton>
</div>
</UiPopoverTrigger>
<UiPopoverContent
v-if="model"
align="start"
side="bottom"
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
>
<div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
<UiSelect @update:model-value="handleMonthChange">
<UiSelectTrigger>
<UiSelectValue :placeholder="monthTitle" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectGroup class="max-h-[50vh]">
<UiSelectItem v-for="(month, i) in sortedMonths" :key="`popover-select-item-${id}-${i}`" :value="i.toString()">
{{ month.name }}
</UiSelectItem>
</UiSelectGroup>
</UiSelectContent>
</UiSelect>
<UiInput v-model="model.year" type="number" />
<!-- <nav class="flex items-center justify-end gap-2">
<UiButton variant="outline" size="icon" class="w-8 h-8" @click="decrementModelMonth()">
<PhCaretLeft size="14" />
</UiButton>
<UiButton variant="outline" size="icon" class="w-8 h-8" @click="incrementModelMonth()">
<PhCaretRight size="14" />
</UiButton>
</nav> -->
<hr class="col-span-2" >
<div class="col-span-2">
<div v-if="monthData" class="grid grid-cols-7 gap-1">
<button
v-for="day in monthData.days"
:key="`popover-day-grid-${id}-${day}`"
class="aspect-square rounded-full text-[.8em] transition-colors"
:class="{
'hover:bg-indigo-200 dark:hover:bg-indigo-700': day !== model.day,
'bg-indigo-500 hover:bg-indigo-700 text-white': day === model.day
}"
@click="setModelDay(day)"
>
{{ day }}
</button>
</div>
</div>
</div>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -1,71 +0,0 @@
<script lang="ts" setup>
import type { CalendarMonth } from "~/models/CalendarMonth";
import type { RPGDate } from "~/models/Date";
import { PhCalendarBlank } from "@phosphor-icons/vue";
const model = defineModel<RPGDate>({ required: true })
const props = defineProps<{
availableMonths: CalendarMonth[]
}>()
/**
*
* @param e The name of today's month
*/
function setTodayMonth(e: string) {
model.value.month = e
}
// When the model changes, get the month index from the month name
watch(model.value, (n, _o) => {
// If the month value is already an index, return early
if (!isNaN(+model.value.month)) return
const monthId = props.availableMonths.findIndex((m) => m.name === n.month)
if (monthId !== -1) {
model.value.month = monthId + 1
}
})
</script>
<template>
<div class="flex gap-2 items-stretch">
<UiInput id="new-month-current-day" v-model="model.day" type="number" name="newMonthCurrentDay" :placeholder="$t('entity.calendar.months.daysNameSingular')" min="1" step="1" class="invalid:border-red-600" />
<UiDropdownMenu>
<UiDropdownMenuTrigger as-child :disabled="props.availableMonths.length < 1">
<UiButton size="sm" variant="secondary">
<PhCalendarBlank size="18" weight="fill" />
<template v-if="props.availableMonths.length < 1">
{{ $t('entity.calendar.months.noneAvailable') }}
</template>
<template v-else-if="model.month && typeof model.month === 'number'">
{{ props.availableMonths[model.month - 1].name }}
</template>
<template v-else>
{{ $t('entity.calendar.months.chooseOne') }}
</template>
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
<UiDropdownMenuLabel>
{{ $t('entity.calendar.months.available') }}
</UiDropdownMenuLabel>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem
v-for="m in props.availableMonths"
:key="m.name"
@click="setTodayMonth(m.name)"
>
{{ m.name }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
<UiInput id="new-month-current-day" v-model="model.year" type="number" name="newMonthCurrentYear" :placeholder="$t('entity.calendar.years.nameSingular')" step="1" class="invalid:border-red-600" />
</div>
</template>

View File

@@ -1,47 +0,0 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
const { revealAdvancedSearch } = useCalendar()
const { isReadOnly } = storeToRefs(useCalendar())
</script>
<template>
<header class="mt-2 grid gap-4 border-border border-b-[1px]">
<div class="px-8 flex items-center justify-between gap-2">
<menu class="flex items-center gap-2">
<li v-if="!isReadOnly">
<LazyCalendarDialogQuickCreateEvent />
</li>
<li>
<CalendarMenuToday />
</li>
<li class="ml-4">
<CalendarCurrentDate />
</li>
</menu>
<menu class="flex items-center gap-2">
<li>
<UiButton search-slash @click="revealAdvancedSearch()">
<PhMagnifyingGlass size="20" weight="light" />
<span>
{{ $t('entity.advancedSearch.title') }}
</span>
</UiButton>
</li>
<li>
<CalendarCategoriesCTA />
</li>
<li>
<CalendarOptionsCTA />
</li>
</menu>
</div>
<div class="ml-8">
<CalendarMenuSubnav />
</div>
</header>
</template>

View File

@@ -1,230 +0,0 @@
<script lang="ts" setup>
import { PhCaretDoubleLeft, PhCaretDoubleRight, PhCaretLeft, PhCaretRight } from "@phosphor-icons/vue"
const { currentDate } = useCalendar()
const { t } = useI18n()
interface DirectionLabels {
pastFar: string
pastNear: string
futureNear: string
futureFar: string
}
const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } =
useCalendar()
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
switch (currentConfig.viewType) {
case "month":
return {
pastFar: t("entity.calendar.years.prevSingular"),
pastNear: t("entity.calendar.months.prevSingular"),
futureNear: t("entity.calendar.months.nextSingular"),
futureFar: t("entity.calendar.years.nextSingular")
}
case "year":
return {
pastFar: t("entity.calendar.decades.prevSingular"),
pastNear: t("entity.calendar.years.prevSingular"),
futureNear: t("entity.calendar.years.nextSingular"),
futureFar: t("entity.calendar.decades.nextSingular")
}
case "decade":
return {
pastFar: t("entity.calendar.centuries.prevSingular"),
pastNear: t("entity.calendar.decades.prevSingular"),
futureNear: t("entity.calendar.decades.nextSingular"),
futureFar: t("entity.calendar.centuries.nextSingular")
}
case "century":
default:
return {
pastFar: t("entity.calendar.millenias.prevSingular"),
pastNear: t("entity.calendar.centuries.prevSingular"),
futureNear: t("entity.calendar.centuries.nextSingular"),
futureFar: t("entity.calendar.millenias.nextSingular")
}
}
})
function toPastFar(): void {
switch (currentConfig.viewType) {
case "month":
decrementViewYear()
break
case "year":
decrementViewYear(10)
break
case "decade":
decrementViewYear(100)
break
case "century":
default:
decrementViewYear(1000)
break
}
}
function toPastNear(): void {
switch (currentConfig.viewType) {
case "month":
decrementViewMonth()
break
case "year":
decrementViewYear()
break
case "decade":
decrementViewYear(10)
break
case "century":
default:
decrementViewYear(100)
break
}
}
function toFutureNear(): void {
switch (currentConfig.viewType) {
case "month":
incrementViewMonth()
break
case "year":
incrementViewYear()
break
case "decade":
incrementViewYear(10)
break
case "century":
default:
incrementViewYear(100)
break
}
}
function toFutureFar(): void {
switch (currentConfig.viewType) {
case "month":
incrementViewYear()
break
case "year":
incrementViewYear(10)
break
case "decade":
incrementViewYear(100)
break
case "century":
default:
incrementViewYear(1000)
break
}
}
</script>
<template>
<div class="flex gap-2">
<div class="grid items-end w-40 px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors">
<ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span>
<template #fallback>
<span class="inline-block">
<UiSkeleton class="h-[19px] w-full rounded-sm" />
</span>
</template>
</ClientOnly>
</div>
<div>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="outline"
size="icon"
class="rounded-t-sm rounded-b-none border-b-0"
@click="toPastFar()"
>
<PhCaretDoubleLeft size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.pastFar }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="outline"
size="icon"
class="rounded-t-sm rounded-b-none border-b-0"
@click="toPastNear()"
>
<PhCaretLeft size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.pastNear }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="outline"
size="icon"
class="rounded-t-sm rounded-b-none border-b-0"
@click="toFutureNear()"
>
<PhCaretRight size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.futureNear }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="outline"
size="icon"
class="rounded-t-sm rounded-b-none border-b-0"
@click="toFutureFar()"
>
<PhCaretDoubleRight size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.futureFar }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
</template>

View File

@@ -1,26 +0,0 @@
<script lang="ts" setup>
import { computed } from "vue"
const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year
})
</script>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton size="sm" variant="secondary" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
{{ $t('entity.calendar.date.today') }}
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</template>

View File

@@ -1,454 +0,0 @@
<script lang="ts" setup>
import {
isCharacter,
type Character,
} from "@/models/Characters"
import type { RPGDateOrder } from "@/models/Date"
import {
isCalendarEvent,
type CalendarEvent,
} from "~/models/CalendarEvent"
import { capitalize } from "@/utils/Strings"
import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core"
import { computed, ref, watch } from "vue"
import { searchUnifier, type SearchMode } from "../SearchMode"
import { PhCaretDown, PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from "@phosphor-icons/vue"
import {
ComboboxAnchor,
ComboboxInput,
ComboboxPortal,
ComboboxRoot,
ComboboxTrigger,
VisuallyHidden
} from "radix-vue"
import SearchList from "./lists/SearchList.vue"
import type { Category } from "~/models/Category"
const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar())
const { characters } = storeToRefs(useCharacters())
const searchInput = shallowRef<HTMLInputElement>()
useFocus(searchInput, { initialValue: true })
const searchQuery = ref<string>("")
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
// If the query changes, resets the pagination
// This prevents active page to be greater than the search results
watch(searchQuery, resetPage)
const selectedEntity = useStorage("se", "events" as SearchMode)
// Order
const selectedOrder = ref<RPGDateOrder>("asc")
function setOrderAsc() {
selectedOrder.value = "asc"
resetPage()
}
function setOrderDesc() {
selectedOrder.value = "desc"
resetPage()
}
// Limit
const currentPage = ref<number>(1)
const itemsPerPage: number = 20
const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage)
const endOfList = computed<number>(() => startOfList.value + itemsPerPage)
/**
* Resets the pagination
*/
function resetPage() {
currentPage.value = 1
}
const searchResults = computed<(Character | CalendarEvent)[]>(() => {
let results: (Character | CalendarEvent)[] = []
let dataToFilter: Character[] | CalendarEvent[] | (Character | CalendarEvent)[]
const shouldFilterCategories = selectedCategories.value.length > 0
// Assign data to loop over and filter
// They are assigned this way for readability
if (selectedEntity.value === "events") {
dataToFilter = allEvents.value
} else if (selectedEntity.value === "characters") {
dataToFilter = characters.value
} else {
dataToFilter = [...allEvents.value, ...characters.value]
}
/**
* TODO: Refactor the categories logic, basically extract the return out of the ifs, like above
*/
results = dataToFilter.filter((item) => {
// Filter calendar events
if (isCalendarEvent(item)) {
const queryString = new String(searchQuery.value)
.replace(searchUnifier, "")
.toLocaleLowerCase()
const hitTitle = item.title
.replace(searchUnifier, "")
.toLocaleLowerCase()
.includes(queryString)
const hitDesc = item.description
?.replace(searchUnifier, "")
.toLocaleLowerCase()
.includes(queryString)
if (!shouldFilterCategories) {
return hitTitle || hitDesc
}
// Handle categories logic
let hitCategories: boolean = false
const allCategories: Category[] = []
if (item.category) {
allCategories.push(item.category)
}
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
allCategories.push(...item.secondaryCategories)
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.findIndex((c) => c.name === selectedCat.name) !== -1
})
return (hitTitle || hitDesc) && hitCategories
}
// Filter characters
if (isCharacter(item)) {
const queryString = new String(searchQuery.value)
.replace(searchUnifier, "")
.toLocaleLowerCase()
const hitTitle = item.name
.replace(searchUnifier, "")
.toLocaleLowerCase()
.includes(queryString)
if (!shouldFilterCategories) {
return hitTitle
}
// Handle categories logic
let hitCategories: boolean = false
const allCategories: Category[] = []
if (item.category) {
allCategories.push(item.category)
}
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
allCategories.push(...item.secondaryCategories)
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat as Category)
})
return hitTitle && hitCategories
}
})
return results
})
/**
* Removes the search query, resets the pagination and removes all selected categories
*/
function resetSearch() {
searchQuery.value = ""
resetPage()
selectedCategories.value = []
}
/**
* Opens the search Uidialog
*/
function openUiDialog() {
isAdvancedSearchOpen.value = true
}
/**
* Closes the search Uidialog
*/
function closeUiDialog() {
isAdvancedSearchOpen.value = false
}
/**
* Switches the selectedEntity
*/
function handleEntitySwitch() {
resetPage()
selectedCategories.value = []
}
// Key combos to deploy modal
const keys = useMagicKeys()
whenever(keys.control_period, () => {
openUiDialog()
})
const searchResultsRef = ref<HTMLElement | null>(null)
const { y: searchResultsY } = useScroll(searchResultsRef)
watch([currentPage, selectedEntity], () => {
searchResultsY.value = 0
})
// Compute categories based on current selectedEntity
const currentCategories = computed(() => categories.value)
const selectedCategories = ref<(Category)[]>([])
const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>("")
const filteredCategories = computed(() => {
// Display original data
if (!currentCategories.value) return []
// If current categories are selected, ignore them
if (!searchCategory.value) return currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
// If we also have a query to filter them by, do that
return currentCategories.value.filter((i) => {
return !selectedCategories.value.includes(i) && i.name.toLocaleLowerCase().includes(searchCategory.value.toLocaleLowerCase())
})
})
// Reactivity rules for category searches
const categoryInput = ref(null)
const categoryInputValue = ref<string>("")
const { focused: categoryInputFocused } = useFocus(categoryInput)
watch(categoryInputFocused, (isFocused) => {
categoryFilterOpened.value = isFocused
})
/**
* Handles the category selections from the TagInput component
*
* @param e Radix Change Event
*/
function handleCategorySelect(e: Category) {
searchCategory.value = ""
selectedCategories.value.push(e)
if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false
}
}
/**
* Removes a category from selection from the TagInput component
*/
function handleCategoryUnselect(e: Category) {
selectedCategories.value.splice(selectedCategories.value.findIndex(sc => sc.name === e.name), 1)
if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false
}
}
</script>
<template>
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
<UiDialogContent
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
:class="{
'bottom-16': searchResults.length > 0
}"
>
<VisuallyHidden>
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
</VisuallyHidden>
<VisuallyHidden>
<UiDialogDescription>
{{ $t('entity.advancedSearch.title') }}
</UiDialogDescription>
</VisuallyHidden>
<!-- UiDialog header -->
<div id="searchForm" class="grid gap-3">
<div class="relative w-full h-fit">
<UiInput
id="search"
ref="searchInput"
v-model:model-value="searchQuery"
type="text"
:placeholder="$t('entity.advancedSearch.ctaPlaceholder')"
class="pl-10 py-6 text-lg"
autocomplete="off"
/>
<span class="absolute start-1 inset-y-0 flex items-center justify-center px-2 opacity-50">
<PhMagnifyingGlass size="20" />
</span>
</div>
<div class="flex items-center justify-between gap-8">
<div>
<UiToggleGroup
v-model="selectedEntity"
type="single"
class="justify-start"
@update:model-value="handleEntitySwitch()"
>
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
{{ $t('entity.calendar.event.namePlural') }}
</UiToggleGroupItem>
<!-- Not used for now -->
<!-- <UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
{{ $t('entity.character.namePlural') }}
</UiToggleGroupItem> -->
</UiToggleGroup>
</div>
<div class="grow flex justify-end items-center gap-1">
<UiTagsInput class="grow px-0 gap-y-1 w-80">
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
<UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name">
<UiTagsInputItemText class="capitalize cursor-pointer" @click="handleCategoryUnselect(item)" />
</UiTagsInputItem>
</div>
<ComboboxRoot
v-model="selectedCategories"
v-model:open="categoryFilterOpened"
v-model:search-term="searchCategory"
class="grow flex items-center gap-y-1 pr-2"
>
<ComboboxAnchor as-child>
<ComboboxInput :placeholder="$t('entity.category.namePlural')" as-child>
<UiTagsInputInput
ref="categoryInput"
v-model="categoryInputValue"
class="min-w-16 px-3"
@keydown.enter.prevent
/>
</ComboboxInput>
</ComboboxAnchor>
<ComboboxTrigger>
<PhCaretDown size="16" />
</ComboboxTrigger>
<ComboboxPortal :to="'#searchForm'">
<UiCommandList
position="popper"
class="w-(--radix-popper-anchor-width) rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
:dismissable="true"
>
<UiCommandEmpty />
<UiCommandGroup>
<UiCommandItem
v-for="cat in filteredCategories"
:key="cat.name"
:value="cat"
@select.prevent="handleCategorySelect(cat)"
>
{{ capitalize(cat.name) }}
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</ComboboxPortal>
</ComboboxRoot>
</UiTagsInput>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
size="icon"
@click="setOrderDesc()"
>
<PhClockCounterClockwise size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ $t('entity.advancedSearch.older') }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
size="icon"
@click="setOrderAsc()"
>
<PhClockClockwise size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ $t('entity.advancedSearch.newer') }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
</div>
<hr >
<div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto">
<SearchList
:results="searchResults"
:current-entity="selectedEntity"
:order="selectedOrder"
:start-at="startOfList"
:end-at="endOfList"
@jumped-to-date="closeUiDialog()"
/>
</div>
<div class="flex justify-end">
<UiPagination
v-model:page="currentPage"
:total="searchResults.length"
:items-per-page="itemsPerPage"
:sibling-count="2"
show-edges
:default-page="1"
>
<UiPaginationList v-slot="{ items }" class="flex items-center gap-1">
<UiPaginationFirst />
<UiPaginationPrev />
<template v-for="(item, index) in items">
<UiPaginationListItem
v-if="item.type === 'page'"
:key="index"
:value="item.value"
as-child
>
<UiButton
class="w-10 h-10 p-0"
:variant="item.value === currentPage ? 'default' : 'outline'"
>
{{ item.value }}
</UiButton>
</UiPaginationListItem>
<UiPaginationEllipsis v-else :key="item.type" :index="index" />
</template>
<UiPaginationNext />
<UiPaginationLast />
</UiPaginationList>
</UiPagination>
</div>
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,121 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent"
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue"
const props = defineProps<{
event: CalendarEvent
}>()
defineEmits<{
(e: "query:date-jump", payload: RPGDate): void
}>()
const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar()
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
</script>
<template>
<button
class="event-callout group relative block w-full text-left py-3 px-4 cursor-pointer transition-colors"
:class="cn(
event.category ? `element-${event.category.color}` : '',
{
'pt-4 is-hidden': event.hidden,
})"
@click="$emit('query:date-jump', event.startDate)"
>
<div class="flex gap-2 items-center mb-1">
<h2 class="font-bold text-lg underline-offset-4 group-hover:underline">
{{ event.title }}
</h2>
<div v-if="event.wiki">
<UiButton variant="link" size="xs" as-child class="text-inherit">
<a :href="event.wiki" target="_blank">
Wiki
<PhArrowSquareOut size="16" weight="fill" />
</a>
</UiButton>
</div>
</div>
<div class="flex gap-2 items-center justify-between mb-1">
<template v-if="!event.endDate">
<p class="font-semibold text-sm opacity-75">
{{ getFormattedDateTitle(event.startDate, true) }}
</p>
</template>
<template v-else>
<p class="font-semibold text-sm opacity-75">
{{
$t('entity.calendar.date.fromTo',
{
startDate: getFormattedDateTitle(event.startDate, true),
endDate: getFormattedDateTitle(event.endDate, true)
}
)
}}
</p>
</template>
<div v-if="event.hidden" class="flex justify-end">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiBadge class="flex gap-1">
<PhEye size="16" weight="fill" /> {{ $t('entity.calendar.event.isHidden') }}
</UiBadge>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.event.hiddenTooltip') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
<div class="mb-1 flex gap-x-2 items-center">
<template v-if="event.location">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhMapPinArea size="16" weight="fill" /> {{ event.location }}
</p>
</template>
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p>
<template v-if="dateDuration">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
</p>
</template>
</div>
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
<ul class="flex gap-1">
<li v-if="event.category">
<UiBadge class="mix-blend-luminosity font-bold bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }}
</UiBadge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ cat.name }}
</UiBadge>
</li>
</ul>
</div>
<div v-if="event.description" class="text-sm">
<hr class="my-2 border-white opacity-50" >
<span class="opacity-75">
{{ event.description }}
</span>
</div>
</button>
</template>

View File

@@ -1,177 +0,0 @@
<script lang="ts" setup>
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent"
import { useElementBounding } from "@vueuse/core"
import { storeToRefs } from "pinia"
import { computed, ref, type ComputedRef } from "vue"
import CalendarEventButton from "../../event/Event.vue"
const props = defineProps<{
date: RPGDate
faded?: boolean
}>()
const calendarTile = ref()
const calendarEventsList = ref()
const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
/**
* All events with a startDate / endDate that starts or ends on the tile
*/
const eventsForTheDay = computed(() => {
return currentEvents.value.filter((currentEvent) => {
return (
areDatesIdentical(currentEvent.startDate, props.date) ||
areDatesIdentical(currentEvent.endDate!, props.date)
)
})
})
/**
* Is the tile on today's date ?
*/
const isDefaultDate = computed(() => {
return areDatesIdentical(props.date, defaultDate)
})
/**
* Is the tile on the hightlighted date ?
*/
const isSelectedDate = computed(() => {
return areDatesIdentical(props.date, selectedDate.value)
})
// Get bounding elements for both tile and events list
const { height: tileHeight, top: tileTop } = useElementBounding(calendarTile)
const { top: tileListTop } = useElementBounding(calendarEventsList)
// Compute the available number of events that can be displayed from refs heights
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
if (!eventsForTheDay.value.length) return 0
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
})
/**
* Events that can fit in the tile's space
*/
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(() => {
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
})
/**
* Remaining events that are not displayed
*
* Used if not all events can be seen in the tile's space
*/
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
</script>
<template>
<div
ref="calendarTile"
class="tile relative text-xs p-2"
:class="{
'text-slate-300 dark:text-slate-500': props.faded,
'text-slate-500 dark:text-slate-300': !props.faded
}"
>
<button
class="relative z-10 group block w-full text-center cursor-pointer"
@click="selectDate(date)"
>
<ClientOnly>
<span
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-indigo-300 dark:group-hover:border-indigo-700"
:class="{
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-indigo-500': isSelectedDate
}"
>
{{ date.day }}
</span>
</ClientOnly>
</button>
<ClientOnly>
<ul
ref="calendarEventsList"
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
:class="{
'opacity-40': props.faded && !isSelectedDate
}"
>
<TransitionGroup name="event">
<li v-for="event in eventsToDisplay" :key="event.id" class="grid w-full pointer-events-auto">
<CalendarEventButton :event :tile-date="date" />
</li>
</TransitionGroup>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
<UiPopover>
<UiPopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
>
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
</button>
</UiPopoverTrigger>
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
<div class="text-center mb-4">
<span
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
>
{{ date.day }}
</span>
</div>
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
<li
v-for="event in eventsForTheDay"
:key="event.title"
class="grid pointer-events-auto"
>
<CalendarEventButton :event :tile-date="date" />
</li>
</ul>
</UiPopoverContent>
</UiPopover>
</li>
</ul>
</ClientOnly>
<ClientOnly>
<LazyCalendarDialogCreateEvent v-if="!isReadOnly" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
// .tile are the direct children of the grid
// We need to get, between each element, a 1px solid line to separate them, for columns and rows
.tile {
border-right-width: 1px;
border-bottom-width: 1px;
}
.event-enter-active {
transition: all 120ms ease-in-out;
}
.event-move,
.event-leave-active {
position: absolute;
transition: all 200ms ease-in-out;
}
.event-enter-from,
.event-leave-to {
opacity: 0;
}
.event-enter-from {
transform: translateX(.15rem);
}
.event-leave-to {
transform: translateX(-.5rem);
}
</style>

View File

@@ -1,40 +0,0 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from "@vueuse/core"
const { currentDate, decrementViewMonth, incrementViewMonth } = useCalendar()
const { currentMonthData } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
const isMovingUp = e.deltaY < 0
if (isMovingUp) {
moveCalendarLeft()
} else {
moveCalendarRight()
}
}
const moveCalendarLeft = useThrottleFn(() => {
decrementViewMonth()
}, 100)
const moveCalendarRight = useThrottleFn(() => {
incrementViewMonth()
}, 100)
</script>
<template>
<div class="grid grid-cols-10" @wheel="handleWheel">
<template v-if="currentMonthData">
<CalendarStateMonthlyDayTile
v-for="day in currentMonthData?.days"
:key="`layout-month-grid-${day}`"
:date="{
day: day,
month: currentDate.currentMonth,
year: currentDate.currentYear
}"
/>
</template>
</div>
</template>

View File

@@ -1,36 +0,0 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from "@vueuse/core"
const { decrementViewYear, incrementViewYear } = useCalendar()
const { sortedMonths: months } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
const isMovingUp = e.deltaY < 0
if (isMovingUp) {
moveCalendarLeft()
} else {
moveCalendarRight()
}
}
const moveCalendarLeft = useThrottleFn(() => {
decrementViewYear()
}, 100)
const moveCalendarRight = useThrottleFn(() => {
incrementViewYear()
}, 100)
</script>
<template>
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
<div class="grid grid-cols-5 gap-x-8 gap-y-16">
<CalendarStateYearlyMonthTile
v-for="month in months"
:key="month.id"
:month
/>
</div>
</div>
</template>

View File

@@ -1,23 +0,0 @@
<script lang="ts" setup>
import type { CalendarMonth } from "~/models/CalendarMonth";
defineProps<{
month: CalendarMonth
}>()
</script>
<template>
<div>
<div class="font-medium">
{{ month.name }}
</div>
<div class="grid grid-cols-7 gap-1">
<CalendarStateYearlyDayTile
v-for="day in month.days"
:key="day"
:month-number="month.position"
:day-number="day"
/>
</div>
</div>
</template>

View File

@@ -1,21 +0,0 @@
<script lang="ts" setup>
import { PhPlusCircle } from "@phosphor-icons/vue";
const emit = defineEmits(["on-click"])
</script>
<template>
<UiCard
class="h-full lg:w-fit transition-all bg-transparent hover:text-slate-900 text-slate-500 dark:hover:text-slate-100 dark:text-slate-500 dark:focus-within:outline-gray-900 hover:-translate-y-0"
has-click
@on-click="emit('on-click')"
>
<UiCardHeader class="h-full justify-center items-center text-center gap-0">
<PhPlusCircle size="38" weight="fill" />
<UiCardTitle class="text-xl">
<slot />
</UiCardTitle>
</UiCardHeader>
</UiCard>
</template>

View File

@@ -1,60 +0,0 @@
<script setup lang="ts">
import { PhCaretRight } from "@phosphor-icons/vue";
const route = useRoute()
export type BreadcrumbItem = {
label?: string;
translateKey?: string;
to?: string;
}
defineProps<{
items?: BreadcrumbItem[];
}>()
</script>
<template>
<UiBreadcrumb>
<UiBreadcrumbList>
<UiBreadcrumbItem v-if="route.path !== '/my'">
<UiBreadcrumbLink as-child>
<NuxtLink to="/my">
{{ $t("breadcrumbs.profile") }}
</NuxtLink>
</UiBreadcrumbLink>
</UiBreadcrumbItem>
<UiBreadcrumbItem v-else>
<UiBreadcrumbPage>
{{ $t("breadcrumbs.profile") }}
</UiBreadcrumbPage>
</UiBreadcrumbItem>
<template v-for="(item, index) in items" :key="index">
<UiBreadcrumbSeparator>
<PhCaretRight />
</UiBreadcrumbSeparator>
<UiBreadcrumbItem>
<UiBreadcrumbLink v-if="item.to" as-child>
<NuxtLink :to="item.to">
<template v-if="item.label">
{{ item.label }}
</template>
<template v-else>
{{ $t(`breadcrumbs.${item.translateKey}`) }}
</template>
</NuxtLink>
</UiBreadcrumbLink>
<UiBreadcrumbPage v-else>
<template v-if="item.label">
{{ item.label }}
</template>
<template v-else>
{{ $t(`breadcrumbs.${item.translateKey}`) }}
</template>
</UiBreadcrumbPage>
</UiBreadcrumbItem>
</template>
</UiBreadcrumbList>
</UiBreadcrumb>
</template>

View File

@@ -1,26 +0,0 @@
<script lang="ts" setup>
type HeadingLevel = "h0" | "h1" | "h2" | "h3"
interface HeadingProps {
level?: HeadingLevel
}
withDefaults(defineProps<HeadingProps>(), {
level: "h2"
})
</script>
<template>
<h1 v-if="level === 'h0'" class="text-4xl md:text-6xl font-bold flex">
<slot />
</h1>
<h1 v-else-if="level === 'h1'" class="text-2xl md:text-4xl font-bold flex">
<slot />
</h1>
<h2 v-else-if="level === 'h2'" class="text-xl md:text-2xl font-bold flex">
<slot />
</h2>
<h3 v-if="level === 'h3'" class="text-lg md:text-xl font-bold flex">
<slot />
</h3>
</template>

View File

@@ -1,21 +0,0 @@
<script lang="ts" setup>
const { lineNb = 3 } = defineProps<{
lineNb?: number
}>()
</script>
<template>
<UiCard class="w-full h-full flex flex-col">
<UiCardHeader>
<UiCardTitle>
<UiSkeleton class="w-2/3 max-w-full h-8" />
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="grow grid gap-2">
<UiSkeleton class="w-full max-w-full h-6" />
<template v-for="(l, i) in lineNb" :key="i">
<UiSkeleton class="max-w-full h-6" :style="`width: ${Math.floor(Math.random() * 100) + 1}%`" />
</template>
</UiCardContent>
</UiCard>
</template>

View File

@@ -1,30 +0,0 @@
<script lang="ts" setup>
import { cn } from "~/lib/utils";
interface SpacingProps {
size?: "xs" | "sm" | "md" | "lg" | "xlg" | "2xl" | "3xl"
}
const props = defineProps<SpacingProps>()
let spacingClass: string
switch (props.size) {
case "lg":
spacingClass = "space-y-4"
break;
case "md":
default:
spacingClass = "space-y-2"
break;
}
</script>
<template>
<div
:class="cn(spacingClass)"
>
<slot />
</div>
</template>

View File

@@ -1,41 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { type RPGColor, rpgColors } from "~/models/Color";
const { id, theme = "normal", position = "popper" } = defineProps<{
id: string
theme?: "normal" | "subtle"
position?: "item-aligned" | "popper" | undefined
}>();
const model = defineModel<RPGColor>({ default: "white" });
</script>
<template>
<UiSelect v-model="model">
<UiSelectTrigger :id :class="cn({ 'h-9': theme === 'subtle' })">
<UiSelectValue
:placeholder="$t('ui.colors.selectOne')"
class="bgc"
:class="cn(`element-${model}`)"
/>
</UiSelectTrigger>
<UiSelectContent :position>
<UiSelectGroup>
<UiSelectItem
v-for="color in rpgColors"
:key="color"
:value="color"
class="bgc"
:class="
cn(
`element-${color}`,
)
"
>
{{ $t(`ui.colors.${color}`) }}
</UiSelectItem>
</UiSelectGroup>
</UiSelectContent>
</UiSelect>
</template>

View File

@@ -1,30 +0,0 @@
<script lang="ts" setup>
import { contentStates, type ContentState } from "~/models/Entity";
defineProps<{
id: string
}>();
const model = defineModel<ContentState>({ default: "draft" });
</script>
<template>
<UiSelect v-model="model">
<UiSelectTrigger :id>
<UiSelectValue
:placeholder="$t('ui.contentState.selectOne')"
/>
</UiSelectTrigger>
<UiSelectContent position="popper">
<UiSelectGroup>
<UiSelectItem
v-for="state in contentStates"
:key="state"
:value="state"
>
{{ $t(`ui.contentState.${state}`) }}
</UiSelectItem>
</UiSelectGroup>
</UiSelectContent>
</UiSelect>
</template>

View File

@@ -1,114 +0,0 @@
<script lang="ts" setup>
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhList } from "@phosphor-icons/vue"
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
const { revealAdvancedSearch } = useCalendar()
const { currentMenu } = 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
}
}
</script>
<template>
<nav class="w-16 py-6 border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 grid grid-rows-[1fr_auto] justify-center transition-colors after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60">
<menu class="flex flex-col gap-4">
<li class="mb-12">
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
<PhList size="27" />
</UiButton>
</li>
<li>
<UiTooltipProvider :delay-duration="50">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
<RouterLink to="/explore">
<PhCompass size="24" weight="fill" />
</RouterLink>
</UiButton>
</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
>
<RouterLink :to="item.to">
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</RouterLink>
</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 />
</nav>
</template>
<style lang="scss" scoped>
nav {
position: relative;
isolation: isolate;
&::after {
display: block;
content: '';
position: absolute;
bottom: 0;
inset-inline: 0;
height: 25rem;
max-height: 100%;
background-image: url("/images/sidebar.png");
background-position: bottom;
background-size: cover;
background-repeat: no-repeat;
mask-image: linear-gradient(to top, black 25%, transparent 50%, transparent);
z-index: -1;
}
}
</style>

View File

@@ -1,17 +0,0 @@
export type SidebarMenuActionType = "event-search"
export type SidebarMenuIcon = "universe" | "world"
export interface SidebarMenuItem {
phIcon: SidebarMenuIcon,
phIconWeight?: "regular" | "light" | "fill" | "duotone" | "bold" | "thin"
highlight?: boolean
tooltip: string
action?: SidebarMenuActionType
to?: string
}
export interface SidebarProps {
menuItems: SidebarMenuItem[],
isHome?: boolean
}

View File

@@ -1,212 +0,0 @@
<script lang="ts" setup>
import { computed } from "vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
import { cn } from "~/lib/utils";
const router = useRouter()
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const userMeta = computed(() => user.value?.user_metadata)
const profileUrl: string = `${useRequestURL().origin}/my/`
const { locale, setLocale } = useI18n()
const menuOpened = ref<boolean>(false)
function closeMenu() {
menuOpened.value = false
}
watch(user, closeMenu)
async function handleGoogleLogin() {
try {
auth.signInWithOAuth({
provider: "google",
options: {
queryParams: {
access_type: "offline",
prompt: "consent"
},
redirectTo: profileUrl
}
})
} catch (err) {
console.log(err)
}
}
async function handleLogout() {
try {
const { error } = await auth.signOut()
if (error) throw error
} catch (err) {
console.log(err)
}
}
type AvailableRoutes = "/my" | "/my/settings"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script>
<template>
<ClientOnly>
<UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger>
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer">
<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="rounded-full border-indigo-200 bg-indigo-700 dark:border-slate-300 dark:bg-neutral-950 dark:hover:bg-slate-50 dark:hover:text-slate-950 cursor-pointer">
<PhUserCircle size="24" />
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
<template v-if="user">
<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>
<p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.anonymousGreeting') }}
</p>
</template>
<UiDropdownMenuSub>
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
<PhPalette size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.appearance') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubTrigger>
<UiDropdownMenuPortal>
<UiDropdownMenuSubContent>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': $colorMode.preference === 'dark' })"
@select.prevent="$colorMode.preference = 'dark'"
>
<PhCheckCircle v-if="$colorMode.preference === 'dark'" size="20" weight="fill" />
<PhMoon v-else size="20" />
<span>
{{ $t('ui.dark') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': $colorMode.preference === 'light' })"
@select.prevent="$colorMode.preference = 'light'"
>
<PhCheckCircle v-if="$colorMode.preference === 'light'" size="20" weight="fill" />
<PhSun v-else size="20" />
<span>
{{ $t('ui.light') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': $colorMode.preference === 'system' })"
@select.prevent="$colorMode.preference = 'system'"
>
<PhCheckCircle v-if="$colorMode.preference === 'system'" size="20" weight="fill" />
<PhLaptop v-else size="20" />
<span>
{{ $t('ui.system') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubContent>
</UiDropdownMenuPortal>
</UiDropdownMenuSub>
<UiDropdownMenuSub>
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
<PhTranslate size="20" />
<span>
{{ $t('ui.sidebarMenu.language') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubTrigger>
<UiDropdownMenuPortal>
<UiDropdownMenuSubContent>
<UiDropdownMenuSubContent>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': locale === 'fr' })"
@select.prevent="setLocale('fr')"
>
<PhCheckCircle v-if="locale === 'fr'" size="20" weight="fill" />
<img v-else src="/images/flag-fr.png" width="20" alt="" loading="eager">
<span>Français</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': locale === 'en' })"
@select.prevent="setLocale('en')"
>
<PhCheckCircle v-if="locale === 'en'" size="20" weight="fill" />
<img v-else src="/images/flag-uk.png" width="20" alt="" loading="eager">
<span>English</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubContent>
</UiDropdownMenuSubContent>
</UiDropdownMenuPortal>
</UiDropdownMenuSub>
<UiDropdownMenuSeparator />
<template v-if="user">
<!-- <UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my/settings')">
<PhGear size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.account') }}
</span>
</UiDropdownMenuItem> -->
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="handleLogout">
<PhSignOut size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.logout') }}
</span>
</UiDropdownMenuItem>
</template>
<UiDropdownMenuItem v-if="!user" class="flex gap-[.5ch] items-center rounded-none" @click="handleGoogleLogin">
<PhSignIn size="18" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.login') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
</ClientOnly>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import { type AlertDialogEmits, type AlertDialogProps, AlertDialogRoot, useForwardPropsEmits } from "radix-vue"
const props = defineProps<AlertDialogProps>()
const emits = defineEmits<AlertDialogEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot />
</AlertDialogRoot>
</template>

View File

@@ -1,20 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button/index"
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>

View File

@@ -1,20 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button/index"
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AlertDialogCancel v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)">
<slot />
</AlertDialogCancel>
</template>

View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import {
AlertDialogContent,
type AlertDialogContentEmits,
type AlertDialogContentProps,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from "radix-vue"
import { cn } from "@/lib/utils"
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<AlertDialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import {
AlertDialogDescription,
type AlertDialogDescriptionProps,
} from "radix-vue"
import { cn } from "@/lib/utils"
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AlertDialogDescription
v-bind="delegatedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</AlertDialogDescription>
</template>

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@@ -1,22 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import { AlertDialogTitle, type AlertDialogTitleProps } from "radix-vue"
import { cn } from "@/lib/utils"
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AlertDialogTitle
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot />
</AlertDialogTitle>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "radix-vue"
const props = defineProps<AlertDialogTriggerProps>()
</script>
<template>
<AlertDialogTrigger v-bind="props">
<slot />
</AlertDialogTrigger>
</template>

View File

@@ -1,9 +0,0 @@
export { default as AlertDialog } from "./AlertDialog.vue"
export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue"
export { default as AlertDialogContent } from "./AlertDialogContent.vue"
export { default as AlertDialogHeader } from "./AlertDialogHeader.vue"
export { default as AlertDialogTitle } from "./AlertDialogTitle.vue"
export { default as AlertDialogDescription } from "./AlertDialogDescription.vue"
export { default as AlertDialogFooter } from "./AlertDialogFooter.vue"
export { default as AlertDialogAction } from "./AlertDialogAction.vue"
export { default as AlertDialogCancel } from "./AlertDialogCancel.vue"

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { AvatarRoot } from "radix-vue"
import { type AvatarVariants, avatarVariant } from "."
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
size?: AvatarVariants["size"]
shape?: AvatarVariants["shape"]
}>(), {
size: "sm",
shape: "circle",
})
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot />
</AvatarRoot>
</template>

View File

@@ -1,24 +0,0 @@
import { type VariantProps, cva } from "class-variance-authority"
export { default as Avatar } from "./Avatar.vue"
export { default as AvatarImage } from "./AvatarImage.vue"
export { default as AvatarFallback } from "./AvatarFallback.vue"
export const avatarVariant = cva(
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
{
variants: {
size: {
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
},
shape: {
circle: "rounded-full",
square: "rounded-md",
},
},
},
)
export type AvatarVariants = VariantProps<typeof avatarVariant>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { type BadgeVariants, badgeVariants } from "."
import { cn } from "@/lib/utils"
const props = defineProps<{
variant?: BadgeVariants["variant"]
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@@ -1,25 +0,0 @@
import { type VariantProps, cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue"
export const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@@ -1,13 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<nav aria-label="breadcrumb" :class="props.class">
<slot />
</nav>
</template>

View File

@@ -1,22 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { MoreHorizontal } from "lucide-vue-next"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
role="presentation"
aria-hidden="true"
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

View File

@@ -1,16 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<li
:class="cn('inline-flex items-center gap-1.5', props.class)"
>
<slot />
</li>
</template>

View File

@@ -1,20 +0,0 @@
<script lang="ts" setup>
import type { PrimitiveProps } from "radix-vue"
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { Primitive } from "radix-vue"
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
as: "a",
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -1,16 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
>
<slot />
</ol>
</template>

View File

@@ -1,19 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('font-normal text-foreground', props.class)"
>
<slot />
</span>
</template>

View File

@@ -1,21 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { ChevronRight } from "lucide-vue-next"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<li
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:w-3 [&>svg]:h-3', props.class)"
>
<slot>
<ChevronRight />
</slot>
</li>
</template>

View File

@@ -1,7 +0,0 @@
export { default as Breadcrumb } from "./Breadcrumb.vue"
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue"
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue"
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue"
export { default as BreadcrumbList } from "./BreadcrumbList.vue"
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue"
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue"

View File

@@ -1,32 +0,0 @@
import { type VariantProps, cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/20",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-10 px-4 py-2",
xs: "h-7 rounded-sm px-2 text-xs",
sm: "h-9 rounded-md px-3 text-sm",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
link?: string
hasClick?: boolean
}>()
const emit = defineEmits(["on-click"])
</script>
<template>
<div
:class="
cn('rounded-lg border bg-card text-card-foreground shadow-xs transition-all isolate', props.class, {
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
props.link || props.hasClick
})
"
>
<slot />
<NuxtLink
v-if="props.link"
:to="props.link"
class="absolute inset-0 z-10 focus-visible:outline-hidden"
/>
<button
v-if="props.hasClick"
class="absolute inset-0 z-10 focus-visible:outline-hidden"
@click="emit('on-click')" />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('flex items-center p-6 border-t-2 border-t-slate-900', props.class)">
<slot />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View File

@@ -1,18 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<h3
:class="
cn('text-2xl font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

View File

@@ -1,6 +0,0 @@
export { default as Card } from "./Card.vue"
export { default as CardHeader } from "./CardHeader.vue"
export { default as CardTitle } from "./CardTitle.vue"
export { default as CardDescription } from "./CardDescription.vue"
export { default as CardContent } from "./CardContent.vue"
export { default as CardFooter } from "./CardFooter.vue"

View File

@@ -1,33 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue"
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "radix-vue"
import { Check } from "lucide-vue-next"
import { cn } from "@/lib/utils"
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Check class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@@ -1 +0,0 @@
export { default as Checkbox } from "./Checkbox.vue"

View File

@@ -1,11 +0,0 @@
export { ComboboxPortal } from "radix-vue"
export { default as Command } from "./Command.vue"
export { default as CommandDialog } from "./CommandDialog.vue"
export { default as CommandEmpty } from "./CommandEmpty.vue"
export { default as CommandGroup } from "./CommandGroup.vue"
export { default as CommandInput } from "./CommandInput.vue"
export { default as CommandItem } from "./CommandItem.vue"
export { default as CommandList } from "./CommandList.vue"
export { default as CommandSeparator } from "./CommandSeparator.vue"
export { default as CommandShortcut } from "./CommandShortcut.vue"

View File

@@ -1,9 +0,0 @@
export { default as Dialog } from "./Dialog.vue"
export { default as DialogClose } from "./DialogClose.vue"
export { default as DialogTrigger } from "./DialogTrigger.vue"
export { default as DialogHeader } from "./DialogHeader.vue"
export { default as DialogTitle } from "./DialogTitle.vue"
export { default as DialogDescription } from "./DialogDescription.vue"
export { default as DialogContent } from "./DialogContent.vue"
export { default as DialogScrollContent } from "./DialogScrollContent.vue"
export { default as DialogFooter } from "./DialogFooter.vue"

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
import { DropdownMenuArrow, type DropdownMenuArrowProps, useForwardProps } from "radix-vue"
const props = defineProps<DropdownMenuArrowProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<DropdownMenuArrow class="fill-border" v-bind="forwardedProps">
<slot />
</DropdownMenuArrow>
</template>

View File

@@ -1,45 +0,0 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue"
import {
DropdownMenuSubTrigger,
type DropdownMenuSubTriggerProps,
useForwardProps
} from "radix-vue"
import { cn } from "@/lib/utils"
import { PhCaretLeft, PhCaretRight } from "@phosphor-icons/vue";
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], arrowDirection?: "left" | "right" }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuSubTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent',
props.class
)
"
>
<template v-if="props.arrowDirection === 'left'">
<PhCaretLeft class="mr-auto h-4 w-4" />
<span class="inline-block">
<slot />
</span>
</template>
<template v-else>
<slot />
<PhCaretRight class="ml-auto h-4 w-4" />
</template>
</DropdownMenuSubTrigger>
</template>

Some files were not shown because too many files have changed in this diff Show More