129 Commits

Author SHA1 Message Date
Alexis
674a38c30d Fixed search shortcut 2025-08-24 11:38:10 +02:00
Alexis
e7cef314a1 Fixed untranslated string 2025-08-24 11:26:11 +02:00
Alexis
806d3a7908 Updated package 2025-08-23 22:45:35 +02:00
Alexis
6251499940 Added sheet for sidebar mobile 2025-08-15 15:37:35 +02:00
Alexis
f3d5db76ac Removed sidebar toggle on public pages 2025-08-12 11:18:47 +02:00
Alexis
08620b5044 Made content body smaller 2025-08-12 11:16:52 +02:00
Alexis
f89cc22e87 Fixed quick create dialog not resetting skeleton 2025-08-12 11:09:28 +02:00
Alexis
51c678d27e Fixed dashboard link hitbox 2025-08-12 10:44:56 +02:00
Alexis
b9b891c61f Fixed nav colors in dark mode 2025-08-12 10:43:49 +02:00
Alexis
df06228e68 Fixed transition render issue 2025-08-12 10:37:37 +02:00
Alexis
44193aa018 Added about link to CTA 2025-08-12 10:34:23 +02:00
Alexis
73eceda96c Changed secondary button color 2025-08-12 10:18:07 +02:00
Alexis
e305b21f80 Changed projects page handle 2025-08-12 10:11:28 +02:00
Alexis
d2b66a42dd Fixed dark mode select hover 2025-08-12 10:07:12 +02:00
Alexis
be32a018c6 Added input focus on entity forms 2025-08-11 20:23:25 +02:00
Alexis
a5a232bac7 Added navbar on public pages 2025-08-11 20:00:04 +02:00
Alexis
20045b125e Changed explore page h2 2025-08-09 21:06:42 +02:00
Alexis
f1a3854d1d Merge branch 'feat/drag-events' into dev 2025-08-09 18:56:57 +02:00
Alexis
cebcb50903 Added tooltip on rich text 2025-08-09 18:55:55 +02:00
Alexis
396215bb91 Fixed some UI issues with drag state 2025-08-09 16:40:50 +02:00
Alexis
7468374bb5 Added update logic for single day events 2025-08-09 16:24:59 +02:00
Alexis
5a421cf07f Merge branch 'feat/editor' into dev 2025-08-08 10:49:14 +02:00
Alexis
25a9099a18 Added rich text to event description 2025-08-08 10:48:29 +02:00
Alexis
c208393f9f Bigger richtext window 2025-08-07 23:01:34 +02:00
Alexis
d75f3b5423 Added control options 2025-08-07 22:55:53 +02:00
Alexis
97ce5b3553 Added simple tiptap component 2025-08-07 22:37:49 +02:00
Alexis
0a35f441e0 Merge branch 'code/cleanup' into dev 2025-08-04 22:43:49 +02:00
Alexis
c8a345406c Updated packages and some other colours 2025-08-04 22:43:38 +02:00
Alexis
119968e7fc Fixed a bunch of color transition issues on theme switch 2025-08-04 21:48:57 +02:00
Alexis
9b8f48ad8e Fixed tooltip position on client side component 2025-08-04 16:45:46 +02:00
Alexis
2199359b96 Fixed badge color 2025-08-04 13:41:43 +02:00
Alexis
422673cc4c Updated color scheme 2025-08-04 12:31:28 +02:00
Alexis
6aad01c96f Updated color scheme and identity 2025-08-03 19:42:43 +02:00
Alexis
0c96466263 Updated data loading strategy on world page 2025-08-02 22:42:23 +02:00
Alexis
7697e818bf Fixed supabase channel conflicts 2025-08-01 17:18:06 +02:00
Alexis
fd766b9f42 Updated packages 2025-07-29 22:45:22 +02:00
Alexis
0e7b60235a Fixed log in animation 2025-07-29 22:30:14 +02:00
Alexis
300507bbed Made sidebar image smaller 2025-07-29 21:59:36 +02:00
Alexis
693f506610 Reworked data fetching for explore page 2025-07-27 18:57:31 +02:00
Alexis
f2076e413c Made about page more reactive 2025-07-27 18:17:13 +02:00
Alexis
6b02629b01 Fixed nuxt database warning 2025-07-27 17:31:21 +02:00
Alexis
1d8b56c58e Merge branch 'upgrade/nuxt-4' into dev 2025-07-27 15:14:33 +02:00
Alexis
8290d02900 Updated nvm version 2025-07-27 15:14:28 +02:00
Alexis
b67f663f05 Updated legal text as well 2025-07-27 15:12:15 +02:00
Alexis
456135d679 Fixed better sqlite 3 not being installed 2025-07-27 15:04:26 +02:00
Alexis
7fdab8601f Migration to nuxt 4
Used codemods CLI and reworked most alias'd path that stopped working
2025-07-27 14:30:30 +02:00
Alexis
68b9a38f83 Updated other major packages 2025-07-26 23:36:10 +02:00
Alexis
77752e8164 Updated nuxt i18n to v10 2025-07-26 23:32:32 +02:00
Alexis
1d74d6f64b Updated zod to v4 2025-07-26 23:18:09 +02:00
Alexis
b0975ee293 Updated median and minor packages 2025-07-26 23:01:08 +02:00
Alexis
33f4f78ea7 Merge branch 'code/cleanup' into dev 2025-07-26 18:47:27 +02:00
Alexis
4a04728183 Fixed about page being auth guarded 2025-07-26 18:47:16 +02:00
Alexis
8cd20aa386 Removed vector from supabase
This resolves all the issues with unhealthy containers, idk why it still doesn't work in 2025
2025-07-17 22:02:37 +02:00
Alexis
8e325196e6 Updated packages 2025-05-13 11:22:27 +02:00
Alexis
47883d1577 More legal stuff changes 2025-05-13 11:05:33 +02:00
Alexis
4433f5f1b6 Fixed some content rules 2025-05-07 14:54:13 +02:00
Alexis
f132e6d1e0 Added container to about page 2025-05-07 14:34:46 +02:00
Alexis
245fb421b3 Fixed redundant part of digit regex 2025-05-07 14:31:35 +02:00
Alexis
46d7aa74f9 Removed useless assignement 2025-05-07 14:26:19 +02:00
Alexis
9e40ab0982 Changed constructor to cast 2025-05-07 14:16:14 +02:00
Alexis
63d5ba2224 Changed sort to toSorted 2025-05-07 14:12:21 +02:00
Alexis
114e235a93 Removed useless type inference 2025-05-07 14:10:51 +02:00
Alexis
f03d952183 Fixed aria combobox attributes 2025-05-07 14:03:49 +02:00
Alexis
45eefbe35d Removed a leftover comment 2025-05-07 13:57:32 +02:00
Alexis
18f332be54 Removed unused imports 2025-05-07 13:45:20 +02:00
Alexis
67b79000f6 Removed some commented code 2025-05-07 13:40:50 +02:00
Alexis
896bac5f83 Changed defineEmits to be more legible 2025-05-07 13:37:23 +02:00
Alexis
09cd25ce33 Removed duplicate character in regex 2025-05-07 13:18:01 +02:00
Alexis
1cf8510a7b First draft of legal page 2025-05-07 13:15:01 +02:00
Alexis
a83096a17b Added scannerwork to gitignore 2025-05-07 13:14:41 +02:00
Alexis
216617a8e6 Updated deps 2025-05-05 16:42:03 +02:00
Alexis
a668fa8d81 Hotfix for previous ui feature 2025-04-30 17:00:28 +02:00
AlexisNP
0fdb1508a4 Merge pull request #80 from AlexisNP/ui/event-visibility-checkbox
UI/event visibility checkbox
2025-04-30 14:05:10 +02:00
Alexis
d7447eb700 Changed toggle to checkbox 2025-04-30 14:02:02 +02:00
AlexisNP
c2712f7b86 Merge pull request #79 from AlexisNP/features/mobile-calendars
Features/mobile calendars
2025-04-29 16:19:14 +02:00
Alexis
6b01ce181d Added back breadcrumb 2025-04-29 16:16:45 +02:00
Alexis
acb5c0d92f Added middleware for closing sidebar on route change 2025-04-29 09:34:58 +02:00
Alexis
387597755d Added sidebar toggle 2025-04-29 09:28:49 +02:00
Alexis
a5f7e1ded9 Added first sidebar draft 2025-04-28 17:18:06 +02:00
Alexis
93a03ca4e2 Removed breadcrumbs on mobile 2025-04-28 12:27:10 +02:00
Alexis
a6fe4c7ea3 Categories appear on more options 2025-04-28 11:29:33 +02:00
Alexis
805972bf3c Fixed css color issue 2025-04-26 21:16:35 +02:00
Alexis
c071e9c33b Added some transitions 2025-04-26 21:01:52 +02:00
Alexis
b430ab2660 Changed head description 2025-04-26 20:32:31 +02:00
Alexis
a6d388f5da Added missing description to event creation dialog 2025-04-26 20:19:11 +02:00
Alexis
a03b7890e6 Made dialog go full screen on mobile 2025-04-25 21:30:40 +02:00
Alexis
b71e715cf5 Fixed call stack issue 2025-04-25 21:06:19 +02:00
Alexis
9ba09c2d2b Fixed black button for day tile 2025-04-25 17:56:56 +02:00
Alexis
b1ff2ec232 Changed today CTA on mobile 2025-04-25 17:53:40 +02:00
Alexis
093b4e7493 Fixed overflow issues on search 2025-04-25 17:36:54 +02:00
Alexis
7b0bb1e939 Made event creation dialog behaviour the same 2025-04-25 17:33:45 +02:00
Alexis
60bbb6f48f Fixed no contrast event buttons 2025-04-25 17:30:50 +02:00
Alexis
f502f80341 Updated list style 2025-04-25 17:25:01 +02:00
Alexis
e634cc8127 Fixed some inconsistencies 2025-04-25 16:56:09 +02:00
Alexis
ee29169f4f Added transitions for client only buttons 2025-04-25 15:38:37 +02:00
Alexis
2d2069cae6 Fixed hydration mismatches 2025-04-25 15:23:03 +02:00
Alexis
f7f12feaec Changed popover to modal on small screens 2025-04-25 14:58:47 +02:00
Alexis
5b8fedba7c Added space-x for alert dialog 2025-04-25 14:17:48 +02:00
Alexis
fa872e6914 Fixed collision padding for calendar events 2025-04-25 14:15:20 +02:00
Alexis
43dd12782b Reduced spacing on calendar menu 2025-04-23 21:59:45 +02:00
Alexis
f1f140f2bc Reduced some spacing on mobile 2025-04-23 21:41:32 +02:00
Alexis
707d4374ef Changed calendar layout for buttons 2025-04-23 16:21:51 +02:00
AlexisNP
048413286f Merge pull request #77 from AlexisNP/upgrade/nuxt
Upgrade/nuxt
2025-04-23 14:11:29 +02:00
Alexis
9d06040c5d Changed muted colour 2025-04-22 22:30:14 +02:00
Alexis
15bfc66087 Removed dark classes for calendar preview 2025-04-22 22:15:49 +02:00
Alexis
e6b2670438 Removed character string 2025-04-22 17:17:13 +02:00
Alexis
e2d2810db6 Event creation form closes if the date changes 2025-04-22 17:07:29 +02:00
Alexis
1827b9118f Changed chip colour for tags input 2025-04-22 15:36:58 +02:00
Alexis
086b997845 Fixed default breadcrumb look 2025-04-22 15:31:41 +02:00
Alexis
73f5e01244 Removed default lucide icons and updated some packages 2025-04-22 14:09:25 +02:00
Alexis
d02bfacd57 Made search bigger 2025-04-18 18:07:13 +02:00
Alexis
5370a9df06 Added color square to event creation forms 2025-04-18 18:03:55 +02:00
Alexis
5bd76495f4 Added color square to category filter 2025-04-18 17:59:49 +02:00
Alexis
7480447420 Standardized tilde imports for non modules 2025-04-18 17:44:21 +02:00
Alexis
034713961b Changed card color for calendars and worlds 2025-04-18 17:35:02 +02:00
Alexis
00224417be Cleaned up most vanilla try catches 2025-04-18 17:11:45 +02:00
Alexis
9b4556245e Fixed hardcoded world id for calendar creation 2025-04-17 22:34:52 +02:00
Alexis
ad8120bdd0 Removed useless lazy directives 2025-04-17 22:14:10 +02:00
Alexis
73bd79c3c5 Fixed hydration error 2025-04-17 21:47:25 +02:00
Alexis
8215791830 Changed h-w to size 2025-04-17 21:41:48 +02:00
AlexisNP
36456ce377 Merge pull request #76 from AlexisNP/upgrade/tailwind-4
Upgrade/tailwind 4
2025-04-17 19:58:02 +02:00
Alexis
015d0c4db4 Moved bgc colors to relevant file 2025-04-17 17:22:01 +02:00
Alexis
392d8cd8ec Fixed dialog animation issues 2025-04-17 17:02:42 +02:00
Alexis
473c69c9a1 Updated packages 2025-04-17 16:55:51 +02:00
Alexis
790deeec5d Updated border and event colors 2025-04-17 16:42:48 +02:00
Alexis
687b96cf12 Moved event and colored element logic to css variables 2025-04-16 19:52:19 +02:00
Alexis
3e973dd0e9 "Working" config with tailwind 4 and shadcn nuxt 2025-04-16 12:14:27 +02:00
Alexis
cc07f898ea Ran upgrade cli 2025-04-16 12:00:38 +02:00
AlexisNP
e5bc5c8a41 Merge pull request #74 from AlexisNP/features/category-management
Features/category management
2025-04-16 10:59:11 +02:00
326 changed files with 12346 additions and 6218 deletions

2
.gitignore vendored
View File

@@ -26,3 +26,5 @@ logs
# Supabase # Supabase
.branches .branches
.temp .temp
.scannerwork/

2
.nvmrc
View File

@@ -1 +1 @@
20.12.0 24.4.0

View File

@@ -32,17 +32,11 @@ const useIdFunction = () => useId()
<div class="h-screen"> <div class="h-screen">
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<NuxtLayout> <ConfigProvider :use-id="useIdFunction">
<ConfigProvider :use-id="useIdFunction"> <NuxtLayout>
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors"> <NuxtPage />
<Sidebar /> </NuxtLayout>
</ConfigProvider>
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
<NuxtPage />
</div>
</div>
</ConfigProvider>
</NuxtLayout>
<ClientOnly> <ClientOnly>
<UiToaster /> <UiToaster />
@@ -54,4 +48,17 @@ const useIdFunction = () => useId()
.wrapper > * { .wrapper > * {
height: 100%; height: 100%;
} }
.has-sidebar {
position: relative;
isolation: isolate;
&::after {
content: '';
display: block;
position: fixed;
inset: 0;
z-index: 30;
}
}
</style> </style>

331
app/assets/_colors.css Normal file
View File

@@ -0,0 +1,331 @@
: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);
}
::selection {
color: var(--color-background);
background: color-mix(in srgb, var(--color-primary), var(--color-foreground) 20%)
}
.event-button,
.event-popover,
.event-callout,
.card-color {
--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-callout hr,
.card-color hr {
transition-property: border-color;
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(--color-foreground);
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));
}
.card-color {
border-color: color-mix(in srgb, var(--bg-color) 30%, var(--color-background));
background-color: color-mix(in srgb, var(--bg-color) 10%, var(--color-background));
hr {
border-color: color-mix(in srgb, var(--bg-color) 30%, var(--color-background))
}
}
.card-color:is(:hover, :focus-visible) {
border-color: color-mix(in srgb, var(--bg-color) 40%, var(--color-background));
background-color: color-mix(in srgb, var(--bg-color) 20%, var(--color-background));
hr {
border-color: color-mix(in srgb, var(--bg-color) 40%, 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);
}

66
app/assets/_content.css Normal file
View File

@@ -0,0 +1,66 @@
.content {
> * + * {
margin-top: .5em;
@variant md {
margin-top: .75rem;
}
}
h1 {
font-size: var(--text-3xl);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-4xl);
}
}
h2 {
font-size: var(--text-xl);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-2xl);
}
}
h3 {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-xl);
}
}
h4 {
font-size: var(--text-md);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-lg);
}
}
p, ul, ol {
font-size: var(--text-sm);
}
/* Normal link */
p a:not([class]) {
color: var(--color-primary);
text-underline-offset: .25rem;
&:hover {
color: var(--color-accent);
text-decoration: underline;
}
}
ul {
list-style-type: square;
padding-inline-start: 1.5rem;
}
hr {
margin-block: 2rem;
}
}

View File

@@ -0,0 +1,10 @@
@theme inline {
--text-2xs: 0.7rem;
--text-2xs--line-height: 0.75rem;
}
@layer utilities {
.flow > * + * {
margin-top: 1em;
}
}

63
app/assets/main.css Normal file
View File

@@ -0,0 +1,63 @@
@import 'tailwindcss';
@config '../../tailwind.config.js';
@import "./theme.css" layer(theme);
@import "./_typography.css" layer(theme);
@import "./_typography.css" layer(utilities);
@import "./_colors.css" layer(theme);
@import "./_colors.css" layer(utilities);
@import "./_content.css";
/*
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,
.fade-group-enter-active,
.fade-group-leave-active {
transition: all .5s ease;
}
.fade-delay-enter-active,
.fade-delay-leave-active,
.fade-group-delay-enter-active,
.fade-group-delay-leave-active {
transition: all .5s ease 1s;
}
.fade-enter-from,
.fade-leave-to,
.fade-delay-enter-from,
.fade-delay-leave-to,
.fade-group-enter-from,
.fade-group-leave-to,
.fade-group-delay-enter-from,
.fade-group-delay-leave-to {
opacity: 0;
visibility: hidden;
}
.fade-group-leave-active {
position: absolute;
}

111
app/assets/theme.css Normal file
View File

@@ -0,0 +1,111 @@
: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%);
--popover-border: rgb(175, 224, 200);
--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: rgb(101, 230, 166);
--primary-foreground: hsl(222.2 84% 4.9%);
--secondary: hsl(210 50% 95%);
--secondary-foreground: hsl(222.2 84% 4.9%);
--accent: hsl(11, 100%, 86%);
--accent-foreground: hsl(222.2 84% 4.9%);
--destructive: hsl(0, 100%, 63%);
--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(0, 0%, 12%);
--muted-foreground: hsl(215 20.2% 65.1%);
--popover: hsl(165, 47%, 5%);
--popover-foreground: hsl(210 40% 98%);
--popover-border: hsl(165, 47%, 20%);
--card: hsl(180 0 4.9%);
--card-foreground: hsl(210 40% 98%);
--border: hsl(0, 0%, 22.5%);
--input: hsl(165, 47%, 20%);
--primary: rgb(101, 230, 166);
--primary-foreground: hsl(180 0 4.9%);
--secondary: hsl(0, 0%, 22.5%);
--secondary-foreground: hsl(210 40% 98%);
--accent: hsl(11, 87%, 69%);
--accent-foreground: hsl(222.2 84% 4.9%);
--destructive: hsl(0, 62%, 49%);
--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-popover-border: var(--popover-border);
--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

@@ -35,12 +35,12 @@ onMounted(() => {
<template> <template>
<div class="h-full w-full relative"> <div class="h-full w-full relative">
<div class="h-full grid grid-rows-[auto,1fr]"> <div class="h-full grid grid-rows-[auto_1fr]">
<CalendarMenu /> <CalendarMenu />
<component :is="currentViewComponent" /> <component :is="currentViewComponent" />
<LazyCalendarSearch /> <CalendarSearch />
<LazyCalendarDialogCategories v-if="!isReadOnly" /> <LazyCalendarDialogCategories v-if="!isReadOnly" />
<LazyCalendarDialogUpdateEvent v-if="!isReadOnly" /> <LazyCalendarDialogUpdateEvent v-if="!isReadOnly" />
<LazyCalendarDialogDeleteEvent v-if="!isReadOnly" /> <LazyCalendarDialogDeleteEvent v-if="!isReadOnly" />

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
import { PhTag } from "@phosphor-icons/vue"
const { toggleCategoriesModal } = useCalendar()
const { isReadOnly } = storeToRefs(useCalendar())
</script>
<template>
<ClientOnly>
<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>
</ClientOnly>
</template>

View File

@@ -3,10 +3,15 @@ import { storeToRefs } from "pinia"
import { computed } from "vue" import { computed } from "vue"
import { PhMapPin } from "@phosphor-icons/vue" import { PhMapPin } from "@phosphor-icons/vue"
import { breakpointsTailwind } from "@vueuse/core"
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar() const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar()) const { selectedDate } = storeToRefs(useCalendar())
const breakpoints = useBreakpoints(
breakpointsTailwind
)
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true)) const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value)) // const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
@@ -16,19 +21,24 @@ const isToday = computed(() => getDifferenceInDays(defaultDate, selectedDate.val
<template> <template>
<ClientOnly> <ClientOnly>
<div class="flex gap-2 items-center"> <div class="grid md:flex md:gap-2 items-center">
<h1 class="text-2xl font-bold flex items-center gap-1"> <h1 class="text-lg md:text-2xl max-md:leading-tight font-bold flex items-center gap-1">
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }} <PhMapPin
:size="breakpoints.md.value ? 26 : 18"
class="max-md:hidden"
weight="light"
/>
{{ mainDateTitle }}
</h1> </h1>
<h2 v-if="!isToday" class="text-xl italic opacity-75"> <h2 v-if="!isToday" class="text-sm max-md:leading-tight md:text-xl italic opacity-75">
{{ dateDifference }} <span class="max-md:hidden"></span> {{ dateDifference }}
</h2> </h2>
</div> </div>
<template #fallback> <template #fallback>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UiSkeleton class="h-8 w-64" /> <UiSkeleton class="h-8 w-64 max-md:max-w-full" />
<UiSkeleton class="h-6 w-28" /> <UiSkeleton class="max-md:hidden h-6 w-28" />
</div> </div>
</template> </template>
</ClientOnly> </ClientOnly>

View File

@@ -1,10 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { breakpointsTailwind } from "@vueuse/core"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { PhCalendarBlank, PhCheckCircle, PhGear } from "@phosphor-icons/vue" import { PhCalendarBlank, PhCheckCircle, PhDotsThreeVertical, PhGear, PhTag } from "@phosphor-icons/vue"
import { computed } from "vue"
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar() const breakpoints = useBreakpoints(
breakpointsTailwind
)
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType, toggleCategoriesModal } = useCalendar()
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType)) const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</script> </script>
@@ -12,15 +16,29 @@ const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
<UiDropdownMenu> <UiDropdownMenu>
<UiDropdownMenuTrigger as-child> <UiDropdownMenuTrigger as-child>
<UiButton variant="secondary" size="icon"> <UiButton variant="secondary" size="icon">
<PhGear size="20" weight="fill" /> <ClientOnly>
<PhGear v-if="breakpoints.md.value" size="20" weight="fill" />
<PhDotsThreeVertical v-else size="26" weight="bold" />
</ClientOnly>
</UiButton> </UiButton>
</UiDropdownMenuTrigger> </UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="40" class="text-right"> <UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="20" class="text-right">
<UiDropdownMenuArrow /> <UiDropdownMenuArrow />
<UiDropdownMenuLabel> <UiDropdownMenuLabel>
{{ $t('entity.calendar.seeOptions') }} {{ $t('entity.calendar.seeOptions') }}
</UiDropdownMenuLabel> </UiDropdownMenuLabel>
<UiDropdownMenuItem
v-if="!breakpoints.md.value"
class="flex gap-[1ch] justify-end items-center"
@click="toggleCategoriesModal(true)"
>
{{ $t('entity.calendar.seeCategories') }}
<PhTag size="18" />
</UiDropdownMenuItem>
<UiDropdownMenuSub> <UiDropdownMenuSub>
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none"> <UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none"> <UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils"
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 cardRef = ref(null)
const isCardHovered = useElementHover(cardRef)
const { focused: isCardFocused } = useFocusWithin(cardRef)
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}`)
const hasActions = computed(() => isOwner.value && props.showActions)
</script>
<template>
<UiCard
ref="cardRef"
class="w-full h-full flex flex-col"
:link="calendarLink"
:class="cn(
calendar.color ? `group card-color element-${calendar.color}` : '',
)"
>
<UiCardHeader class="gap-4">
<UiCardTitle
class="text-xl"
:class="cn({
'pr-18': hasActions
})"
>
{{ 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="hasActions && (isCardHovered || isCardFocused)"
class="flex gap-2 absolute top-4 right-4 z-20"
>
<Transition name="fade" appear>
<UiButton size="icon" variant="outline" class="size-8 border-foreground/20 group-hover:border-foreground/30 hover:text-background hover:border-foreground hover:bg-foreground" @click="emit('on-edit')">
<PhPencil size="15" weight="fill" />
</UiButton>
</Transition>
<Transition name="fade" appear>
<UiButton size="icon" variant="outline" class="size-8 border-foreground/25 group-hover:border-foreground/40 hover:text-background hover:border-rose-500 hover:bg-rose-500" @click="emit('on-delete')">
<PhTrash size="15" weight="fill" />
</UiButton>
</Transition>
</div>
</UiCardContent>
<hr>
<UiCardFooter class="border-t-0">
<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

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
const { revealAdvancedSearch } = useCalendar()
const breakpoints = useBreakpoints(
breakpointsTailwind
)
</script>
<template>
<ClientOnly>
<Transition name="fade" appear>
<UiButton
:search-slash="breakpoints.lg.value"
:size="breakpoints.lg.value ? 'default' : 'icon'"
@click="revealAdvancedSearch()"
>
<PhMagnifyingGlass size="20" weight="light" />
<span v-if="breakpoints.lg.value">
{{ $t('entity.advancedSearch.title') }}
</span>
</UiButton>
</Transition>
</ClientOnly>
</template>

View File

@@ -1,3 +1,3 @@
export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-'']/g) export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-']/g)
export type SearchMode = "characters" | "events" | undefined export type SearchMode = "characters" | "events" | undefined

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
defineProps<{ defineProps<{
categories: Category[] categories: Category[]

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
defineProps<{ defineProps<{
category: Category category: Category

View File

@@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue" import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
const { t } = useI18n()
const { categories } = defineProps<{ const { categories } = defineProps<{
categories: Category[] categories: Category[]
}>() }>()
@@ -27,7 +25,7 @@ function closeDeleteDialog() {
<ScrollAreaRoot class="h-36 grow overflow-hidden "> <ScrollAreaRoot class="h-36 grow overflow-hidden ">
<ScrollAreaViewport class="w-full h-full pr-4" as-child> <ScrollAreaViewport class="w-full h-full pr-4" as-child>
<div class="[&:last-child]:border-0"> <div class="last:border-0">
<CalendarCategoryTableRow <CalendarCategoryTableRow
v-for="item in sortedCategories" v-for="item in sortedCategories"
:key="item.id" :key="item.id"
@@ -53,7 +51,7 @@ function closeDeleteDialog() {
<UiDialogContent <UiDialogContent
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="true" :trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950" class="min-w-96"
@escape-key-down="closeDeleteDialog" @escape-key-down="closeDeleteDialog"
@focus-outside="closeDeleteDialog" @focus-outside="closeDeleteDialog"
@interact-outside="closeDeleteDialog" @interact-outside="closeDeleteDialog"

View File

@@ -2,7 +2,7 @@
import { PhPlus } from "@phosphor-icons/vue" import { PhPlus } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast" import { useToast } from "~/components/ui/toast"
import { ToastLifetime } from "~/components/ui/toast/use-toast" import { ToastLifetime } from "~/components/ui/toast/use-toast"
import { cn } from "~/lib/utils" import { cn } from "@/lib/utils"
const { toast } = useToast() const { toast } = useToast()
const { t } = useI18n() const { t } = useI18n()
@@ -86,7 +86,7 @@ async function submitNew() {
<div class="border-y-[1px] border-b-foreground/10 mr-4"> <div class="border-y-[1px] border-b-foreground/10 mr-4">
<form <form
ref="rowRef" 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" class="grid grid-cols-12 items-center gap-4 p-1 bg-primary/0 hover:bg-primary/15"
@submit.prevent="submitNew" @submit.prevent="submitNew"
> >
<div v-if="currentMode === 'add'" class="col-span-1" /> <div v-if="currentMode === 'add'" class="col-span-1" />
@@ -109,7 +109,7 @@ async function submitNew() {
ref="inputRef" ref="inputRef"
v-model="categorySkeleton.name" v-model="categorySkeleton.name"
type="text" type="text"
class="p-1 h-full w-full bg-transparent focus-visible:outline-none italic" class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
> >
</template> </template>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { PhListBullets } from "@phosphor-icons/vue";
</script> </script>
<template> <template>
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-foreground/10 font-bold mr-4"> <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"> <div class="col-span-1 flex items-end">
<PhListBullets size="20" /> <PhListBullets size="20" />
</div> </div>

View File

@@ -2,8 +2,8 @@
import { PhCheck, PhTrash } from "@phosphor-icons/vue" import { PhCheck, PhTrash } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast" import { useToast } from "~/components/ui/toast"
import { ToastLifetime } from "~/components/ui/toast/use-toast" import { ToastLifetime } from "~/components/ui/toast/use-toast"
import { cn } from "~/lib/utils" import { cn } from "@/lib/utils"
import type { Category } from "~/models/Category" import type { Category } from "@@/models/Category"
const { toast } = useToast() const { toast } = useToast()
const { t } = useI18n() const { t } = useI18n()
@@ -13,7 +13,7 @@ const { category } = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "on-delete-category", payload: Category): void "on-delete-category": [payload: Category]
}>() }>()
type RowMode = "edit" | "view" type RowMode = "edit" | "view"
@@ -103,8 +103,8 @@ function handleQueryDelete() {
<form <form
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10" class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
:class="cn( :class="cn(
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' }, { 'bg-primary/10 hover:bg-primary/20': currentMode === 'edit' },
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' } { 'bg-primary/0 hover:bg-primary/15': currentMode !== 'edit' }
)" )"
@submit.prevent="submitUpdate" @submit.prevent="submitUpdate"
> >
@@ -127,7 +127,7 @@ function handleQueryDelete() {
ref="inputRef" ref="inputRef"
v-model="categorySkeleton.name" v-model="categorySkeleton.name"
type="text" type="text"
class="p-1 h-full w-full bg-transparent focus-visible:outline-none italic" class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
> >
</template> </template>
</div> </div>
@@ -139,7 +139,7 @@ function handleQueryDelete() {
> >
<span <span
class="bgc" class="bgc"
:class="cn(`bgc-${category.color}`)" :class="cn(`element-${category.color}`)"
> >
{{ $t(`ui.colors.${category.color}`) }} {{ $t(`ui.colors.${category.color}`) }}
</span> </span>
@@ -162,9 +162,9 @@ function handleQueryDelete() {
v-if="currentMode === 'edit' && categorySkeleton" v-if="currentMode === 'edit' && categorySkeleton"
> >
<UiButton <UiButton
variant="secondary" variant="default"
size="icon" size="icon"
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white" class="w-6 h-6 rounded-full"
:title="$t('ui.actions.edit')" :title="$t('ui.actions.edit')"
@click="submitUpdate" @click="submitUpdate"
> >
@@ -173,9 +173,9 @@ function handleQueryDelete() {
</li> </li>
<li v-else-if="rowHovered"> <li v-else-if="rowHovered">
<UiButton <UiButton
variant="secondary" variant="destructive"
size="icon" size="icon"
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white" class="w-6 h-6 rounded-full"
:title="$t('ui.actions.delete')" :title="$t('ui.actions.delete')"
@click="handleQueryDelete" @click="handleQueryDelete"
> >

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue"; import { PhX } from "@phosphor-icons/vue";
import type { World } from "~/models/World"; import type { World } from "@@/models/World";
defineProps<{ defineProps<{
world: World, world: World,
@@ -41,7 +41,7 @@ function handleClose() {
<PhX size="20" /> <PhX size="20" />
</UiButton> </UiButton>
<CalendarFormCreate @on-changed-name="onChangedName" @on-close="handleClose" /> <CalendarFormCreate v-if="world.id" :world-id="world.id" @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent> </UiAlertDialogContent>
</UiAlertDialog> </UiAlertDialog>
</template> </template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar()) const { eventSkeleton, operationInProgress, currentRPGDate } = storeToRefs(useCalendar())
const { resetSkeleton } = useCalendar() const { resetSkeleton } = useCalendar()
const popoverOpen = ref(false) const popoverOpen = ref(false)
@@ -39,6 +39,13 @@ function handleClosing() {
popoverOpen.value = false popoverOpen.value = false
resetSkeleton() resetSkeleton()
} }
// If the date changes, the popover should close
// Only for months for now (because it's the only view)
watch(currentRPGDate, () => {
if (!popoverOpen.value) return
handleClosing()
})
</script> </script>
<template> <template>
@@ -52,7 +59,7 @@ function handleClosing() {
:collision-padding="60" :collision-padding="60"
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="true" :trap-focus="true"
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950" class="pl-3 w-[30rem] max-w-full"
@escape-key-down.prevent="handleClosing" @escape-key-down.prevent="handleClosing"
@pointer-down-outside.prevent="handleClosing" @pointer-down-outside.prevent="handleClosing"
> >

View File

@@ -2,7 +2,7 @@
import { PhCircleNotch } from "@phosphor-icons/vue"; import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast"; import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast"; import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
const { toast } = useToast() const { toast } = useToast()
const { t } = useI18n() const { t } = useI18n()
@@ -22,26 +22,25 @@ async function handleAction(): Promise<void> {
isLoading.value = true isLoading.value = true
try { const { error } = await tryCatch($fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" }))
await $fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" })
emit("on-close")
if (error) {
toast({ toast({
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }), title: error.message,
variant: "success", variant: "destructive"
duration: ToastLifetime.SHORT
}) })
} catch (err) {
console.log(err)
if (err instanceof Error) {
toast({
title: err.message,
variant: "destructive"
})
}
} finally {
isLoading.value = false isLoading.value = false
return
} }
toast({
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
variant: "success",
duration: ToastLifetime.SHORT
})
emit("on-close")
isLoading.value = false
} }
/** /**
@@ -77,14 +76,6 @@ function handleClosing() {
</UiAlertDialogDescription> </UiAlertDialogDescription>
<form @submit.prevent="handleAction"> <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"> <footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="handleClosing"> <UiButton type="button" size="sm" variant="outline" @click="handleClosing">
{{ $t('ui.action.back') }} {{ $t('ui.action.back') }}

View File

@@ -27,7 +27,7 @@ function handleClosing(e: Event): void {
<UiAlertDialogContent <UiAlertDialogContent
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="true" :trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950" class="min-w-96"
@escape-key-down="handleClosing" @escape-key-down="handleClosing"
@focus-outside="handleClosing" @focus-outside="handleClosing"
@interact-outside="handleClosing" @interact-outside="handleClosing"

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhPlus } from "@phosphor-icons/vue";
import { VisuallyHidden } from "radix-vue";
const isDialogOpen = ref<boolean>(false);
const { resetSkeleton } = useCalendar();
// Watch the popover state
watch(isDialogOpen, (hasOpened, _o) => {
if (hasOpened) {
resetSkeleton()
}
})
// Toggles the dialog
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
/**
* Prevents the modal from closing if's still loading
*/
function handleClosing() {
setTimeout(() => resetSkeleton(), 100)
}
const breakpoints = useBreakpoints(
breakpointsTailwind
)
</script>
<template>
<ClientOnly>
<Transition name="fade" appear>
<UiButton
class="max-md:fixed max-md:bottom-8 max-md:right-8 max-md:z-50 max-md:size-14 max-md:rounded-xl"
:size="breakpoints.md.value ? 'default' : 'icon'"
@click="toggleDialog"
>
<PhPlus :size="breakpoints.md.value ? 18 : 24" weight="bold" />
<strong v-if="breakpoints.md.value" class="font-semibold">
{{ $t("entity.calendar.event.newEvent") }}
</strong>
</UiButton>
</Transition>
</ClientOnly>
<UiDialog v-model:open="isDialogOpen">
<UiDialogContent
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
:trap-focus="true"
@escape-key-down="handleClosing"
@pointer-down-outside="handleClosing"
>
<UiDialogTitle class="max-md:mb-8">
{{ $t("entity.calendar.event.addSingle") }}
</UiDialogTitle>
<VisuallyHidden>
<UiDialogDescription>
{{ $t("entity.calendar.event.addSingleDescription") }}
</UiDialogDescription>
</VisuallyHidden>
<CalendarFormCreateEvent @event-created="toggleDialog" />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue"; import { PhX } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
import type { World } from "~/models/World"; import type { World } from "@@/models/World";
const props = defineProps<{ const props = defineProps<{
calendar: Calendar | null, calendar: Calendar | null,

View File

@@ -25,13 +25,13 @@ function handleClosing() {
<UiDialogContent <UiDialogContent
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="true" :trap-focus="true"
class="pl-3 min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950" class="pl-3 md:min-w-96 max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
@escape-key-down="handleClosing" @escape-key-down="handleClosing"
@focus-outside="handleClosing" @focus-outside="handleClosing"
@interact-outside="handleClosing" @interact-outside="handleClosing"
@pointer-down-outside="(e) => e.preventDefault()" @pointer-down-outside.prevent
> >
<header class="pl-8 grid gap-y-2"> <header class="pl-8 grid gap-y-2 max-md:mb-8">
<UiDialogTitle> <UiDialogTitle>
{{ $t('entity.calendar.event.editDialog.title') }} {{ $t('entity.calendar.event.editDialog.title') }}
</UiDialogTitle> </UiDialogTitle>

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { import {
PhHourglassMedium, PhHourglassMedium,
@@ -13,6 +14,7 @@ import {
PhDotsThreeOutlineVertical, PhDotsThreeOutlineVertical,
PhEye PhEye
} from "@phosphor-icons/vue" } from "@phosphor-icons/vue"
import type { CollisionPadding } from "@@/models/Popover"
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar() const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar()) const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
@@ -24,6 +26,22 @@ const props = defineProps<{
isEndEvent?: boolean isEndEvent?: boolean
}>() }>()
const breakpoints = useBreakpoints(
breakpointsTailwind
)
const collisionPadLg: CollisionPadding = {
top: 50,
bottom: 50,
left: 50,
right: 50,
}
const collisionPad: CollisionPadding = {
top: 80,
bottom: 80,
left: 12.5,
right: 12.5,
}
// Ref for the popover // Ref for the popover
const eventDetails = ref<HTMLElement>() const eventDetails = ref<HTMLElement>()
@@ -62,17 +80,14 @@ function deployDeleteModal() {
<template> <template>
<UiPopoverContent <UiPopoverContent
class="w-96" class="max-w-full w-96 event-popover"
:align="'center'" :align="'center'"
:align-offset="50" :align-offset="breakpoints.lg.value ? 50 : 25"
:side="'left'" :side-offset="8"
:collision-padding="60" :side="breakpoints.lg.value ? 'left' : 'top'"
:hide-when-detached="true" :collision-padding="breakpoints.lg.value ? collisionPadLg : collisionPad"
:class="cn( :class="cn(
event.category ? `event-details-${event.category.color}` : '', event.category ? `element-${event.category.color}` : '',
{
'event-details': event.category
}
)" )"
@focus-outside.prevent @focus-outside.prevent
> >
@@ -143,10 +158,10 @@ function deployDeleteModal() {
</template> </template>
<template v-if="event.description"> <template v-if="event.description">
<hr class="border-slate-500 mt-2" > <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"> <div class="max-h-48 overflow-y-auto mt-2 text-sm opacity-80">
{{ event.description }} <div class="content-editor" v-html="event.description" />
</div> </div>
</template> </template>
</div> </div>
@@ -172,19 +187,19 @@ function deployDeleteModal() {
</menu> </menu>
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2"> <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> <UiBadge as-child>
<button <button
class="flex gap-1" class="flex gap-1 cursor-pointer"
title="Naviguer au début" :title="$t('entity.calendar.event.gotoStart')"
@click="handleJumpToDate(event.startDate!)" @click="handleJumpToDate(event.startDate!)"
> >
<PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }} <PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }}
</button> </button>
</UiBadge> </UiBadge>
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child title="Naviguer à la fin"> <UiBadge as-child>
<button <button
class="flex gap-1" class="flex gap-1 cursor-pointer"
title="Naviguer à la fin" :title="$t('entity.calendar.event.gotoEnd')"
@click="handleJumpToDate(event.endDate!)" @click="handleJumpToDate(event.endDate!)"
> >
<PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }} <PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }}

View File

@@ -1,15 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "~/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
import { ja } from "zod/v4/locales";
const props = defineProps<{ const props = defineProps<{
event: CalendarEvent event: CalendarEvent
tileDate: RPGDate tileDate: RPGDate
}>() }>()
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar() const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')
const { lastActiveEvent } = storeToRefs(useCalendar())
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal, resetSkeleton } = useCalendar()
const { lastActiveEvent, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate)) const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate)) const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
@@ -49,25 +52,47 @@ onMounted(() => {
handleDelete() handleDelete()
}) })
}) })
// Draggable logic
const isTileDragging = shallowRef(false)
function handleDragStart() {
isTileDragging.value = true
isDraggingEvent.value = true
eventSkeleton.value = structuredClone(toRaw(props.event))
handleClosePopover()
}
function handleDragEnd() {
isTileDragging.value = false
isDraggingEvent.value = false
}
</script> </script>
<template> <template>
<UiPopover v-model:open="isPopoverDetailsOpen"> <UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child> <UiPopoverTrigger as-child>
<button <button
class="event text-xs px-2 py-1 block w-full text-left rounded-sm transition-colors outline-offset-1" ref="buttonRef"
class="event-button text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left rounded-sm transition-opacity outline-offset-1 cursor-pointer"
:draggable="event.endDate ? 'false' : 'true'"
:class=" :class="
cn( cn(
event.category ? `event-${event.category.color}` : '', `element-${event.category?.color}`,
{ {
'is-hidden': event.hidden, 'is-hidden': event.hidden,
'rounded-r-none': isStartEvent, 'rounded-r-none': isStartEvent,
'rounded-l-none': isEndEvent, 'rounded-l-none': isEndEvent,
},
{
'opacity-60': isTileDragging,
'opacity-100': !isTileDragging
} }
) )
" "
@dblclick="handleDoubleClick" @dblclick="handleDoubleClick"
@keydown.delete="handleDelete" @keydown.delete="handleDelete"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
> >
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto"> <div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
{{ eventTitle }} {{ eventTitle }}
@@ -75,7 +100,7 @@ onMounted(() => {
</button> </button>
</UiPopoverTrigger> </UiPopoverTrigger>
<LazyCalendarEventDetails <CalendarEventDetails
:event :event
:spans-multiple-days :spans-multiple-days
:is-start-event :is-start-event

View File

@@ -1,8 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue"; 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 props = defineProps<{
worldId: number
}>()
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], categories: [], state: "draft", color: "white" }
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton }) const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
onMounted(() => { onMounted(() => {
@@ -12,6 +16,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today" type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global") const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/** /**
* === Current date === * === Current date ===
*/ */
@@ -38,26 +45,26 @@ const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeleton
const isCreatingCalendar = ref<boolean>(false) const isCreatingCalendar = ref<boolean>(false)
async function handleSubmit() { async function handleSubmit() {
try { isCreatingCalendar.value = true
isCreatingCalendar.value = true
await $fetch("/api/calendars/create", { method: "POST", body: { ...calendarSkeleton.value, worldId: 1 } })
emit("on-close") const { error } = await tryCatch($fetch("/api/calendars/create", { method: "POST", body: { ...calendarSkeleton.value, worldId: props.worldId } }))
} catch (err) {
console.log(err) if (error) {
} finally { console.log(error.message)
isCreatingCalendar.value = false isCreatingCalendar.value = false
return
} }
emit("on-close")
isCreatingCalendar.value = false
} }
/** /**
* === Watch for name changes to display above === * Watch for name changes to display above
*/ */
const emit = defineEmits<{ const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars "on-changed-name": [calendarName: string]
(e: "on-changed-name", calendarName: string): void "on-close": []
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>() }>()
/** Hook to emit a debounced event for the changed skeleton name */ /** Hook to emit a debounced event for the changed skeleton name */
@@ -99,13 +106,14 @@ function handleFormCancel() {
</UiTabsList> </UiTabsList>
<UiTabsContent value="global" class="grid gap-4"> <UiTabsContent value="global" class="grid gap-4">
<input <input
ref="newCalendarName"
id="new-calendar-name" id="new-calendar-name"
v-model="calendarSkeleton.name" v-model="calendarSkeleton.name"
type="text" type="text"
name="new-calendar-name" name="new-calendar-name"
required required
:placeholder="$t('common.title')" :placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
@input="handleNameChange" @input="handleNameChange"
> >

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue" import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue"
const { t } = useI18n() const { t } = useI18n()
@@ -29,17 +29,16 @@ async function handleSubmit() {
isLoading.value = true isLoading.value = true
try { const { error } = await tryCatch(submitSkeleton())
await submitSkeleton()
emit("event-created") if (error) {
} catch (err) { formErrors.message = error.message
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false isLoading.value = false
return
} }
emit("event-created")
isLoading.value = false
} }
/** /**
@@ -67,7 +66,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.title')" :placeholder="$t('entity.calendar.event.title')"
:maxlength="120" :maxlength="120"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,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-none focus-visible:border-blue-600 invalid:border-red-500" 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-primary invalid:border-destructive"
> >
<div class="mt-2 mb-1 text-xs opacity-50"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }} {{ t('entity.calendar.event.patterns.title') }}
@@ -75,14 +74,11 @@ function handleCancel() {
</div> </div>
<div class="col-span-2 my-2 pl-8"> <div class="col-span-2 my-2 pl-8">
<textarea <InputEditor
id="new-event-description" disable-blocks
v-model="eventSkeleton.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-none focus-visible:border-blue-600 invalid:border-red-500"
/> />
<div class="mt-2 mb-1 text-xs opacity-50"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }} {{ t('entity.calendar.event.patterns.description') }}
</div> </div>
@@ -120,14 +116,6 @@ function handleCancel() {
</div> </div>
</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="col-span-2 mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
@@ -141,7 +129,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.addLocation')" :placeholder="$t('entity.calendar.event.addLocation')"
:maxlength="160" :maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,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-none focus-visible:border-blue-600" class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
> >
<div class="mt-2 mb-1 text-xs opacity-50"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }} {{ t('entity.calendar.event.patterns.location') }}
@@ -156,14 +144,9 @@ function handleCancel() {
<PhEyeClosed v-else size="18" /> <PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" /> <UiCheckbox id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility"> <UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden"> {{ $t('entity.calendar.event.hiddenLabel') }}
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel> </UiLabel>
</div> </div>
</div> </div>

View File

@@ -22,21 +22,20 @@ async function handleAction(): Promise<void> {
const categoryName = categorySkeleton.value?.name const categoryName = categorySkeleton.value?.name
try { const { error } = await tryCatch(deleteCategoryFromSkeleton())
await deleteCategoryFromSkeleton()
toast({ if (error) {
title: t("entity.category.deletedToast.title", { category: categoryName }), formErrors.message = error.message
variant: "success",
duration: ToastLifetime.MEDIUM
})
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false isLoading.value = false
return
} }
toast({
title: t("entity.category.deletedToast.title", { category: categoryName }),
variant: "success",
duration: ToastLifetime.MEDIUM
})
isLoading.value = false
} }
/** /**

View File

@@ -22,24 +22,23 @@ async function handleAction(): Promise<void> {
const eventTitle = eventSkeleton.value.title const eventTitle = eventSkeleton.value.title
try { const { error } = await tryCatch(deleteEventFromSkeleton())
await deleteEventFromSkeleton()
isDeleteEventModalOpen.value = false if (error) {
resetSkeleton() formErrors.message = error.message
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 isLoading.value = false
return
} }
isDeleteEventModalOpen.value = false
resetSkeleton()
toast({
title: t("entity.calendar.event.deletedToast.title", { event: eventTitle }),
variant: "success",
duration: ToastLifetime.MEDIUM
})
isLoading.value = false
} }
/** /**

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue"; import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
import type { World } from "~/models/World"; import type { World } from "@@/models/World";
const props = defineProps<{ const props = defineProps<{
calendar: Calendar | null, calendar: Calendar | null,
@@ -17,6 +17,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today" type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global") const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/** /**
* === Form Validation === * === Form Validation ===
*/ */
@@ -34,26 +37,30 @@ const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeleton
const isUpdatingCalendar = ref<boolean>(false) const isUpdatingCalendar = ref<boolean>(false)
async function handleSubmit() { async function handleSubmit() {
try { isUpdatingCalendar.value = true
isUpdatingCalendar.value = true
await $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.world?.id } })
emit("on-close") const fetchBody = { ...calendarSkeleton.value, worldId: props.world?.id }
} catch (err) {
console.log(err) const { error } = await tryCatch(
} finally { $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: fetchBody })
)
if (error) {
console.log(error.message)
isUpdatingCalendar.value = false isUpdatingCalendar.value = false
return
} }
emit("on-close")
isUpdatingCalendar.value = false
} }
/** /**
* === Watch for name changes to display above === * Watch for name changes to display above
*/ */
const emit = defineEmits<{ const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars "on-changed-name": [calendarName: string]
(e: "on-changed-name", calendarName: string): void "on-close": []
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>() }>()
/** Hook to emit a debounced event for the changed skeleton name */ /** Hook to emit a debounced event for the changed skeleton name */
@@ -78,30 +85,17 @@ function handleFormCancel() {
{{ $t('entity.calendar.actionDialog.tabs.general.title') }} {{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div> </div>
</UiTabsTrigger> </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> </UiTabsList>
<UiTabsContent value="global" class="grid gap-4"> <UiTabsContent value="global" class="grid gap-4">
<input <input
ref="newCalendarName"
id="new-calendar-name" id="new-calendar-name"
v-model="calendarSkeleton.name" v-model="calendarSkeleton.name"
type="text" type="text"
name="new-calendar-name" name="new-calendar-name"
required required
:placeholder="$t('common.title')" :placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
@input="handleNameChange" @input="handleNameChange"
> >
@@ -121,12 +115,6 @@ function handleFormCancel() {
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" /> <InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div> </div>
</UiTabsContent> </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> </UiTabs>
<footer class="flex justify-end gap-2 mt-6"> <footer class="flex justify-end gap-2 mt-6">

View File

@@ -2,7 +2,7 @@
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue" import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast"; import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast"; import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { APIError } from "~/models/Errors"; import type { APIError } from "@@/models/Errors";
const emit = defineEmits(["event-updated"]) const emit = defineEmits(["event-updated"])
@@ -23,19 +23,11 @@ async function handleAction() {
isLoading.value = true isLoading.value = true
try { const { error } = await tryCatch(updateEventFromSkeleton())
await updateEventFromSkeleton()
emit("event-updated") if (error) {
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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const apiError = (err as any).data as APIError const apiError = (error as any).data as APIError
apiError.data.errors.forEach((error) => { apiError.data.errors.forEach((error) => {
toast({ toast({
@@ -45,10 +37,21 @@ async function handleAction() {
duration: ToastLifetime.MEDIUM, duration: ToastLifetime.MEDIUM,
}) })
}) })
} finally {
resetSkeleton()
isLoading.value = false isLoading.value = false
return
} }
emit("event-updated")
toast({
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
variant: "success",
duration: ToastLifetime.SHORT
})
isLoading.value = false
resetSkeleton()
} }
/** /**
@@ -79,7 +82,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.title')" :placeholder="$t('entity.calendar.event.title')"
:minlength="3" :minlength="3"
:maxlength="120" :maxlength="120"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500" 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-primary invalid:border-destructive"
> >
<div class="mt-2 mb-1 text-xs opacity-50"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }} {{ t('entity.calendar.event.patterns.title') }}
@@ -89,14 +92,11 @@ function handleCancel() {
</div> </div>
<div class="col-span-2 my-2 ml-8"> <div class="col-span-2 my-2 ml-8">
<textarea <InputEditor
id="new-event-description" disable-blocks
v-model="eventSkeleton.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-none focus-visible:border-blue-600 invalid:border-red-500"
/> />
<div class="mt-2 mb-1 text-xs opacity-50"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }} {{ t('entity.calendar.event.patterns.description') }}
</div> </div>
@@ -136,16 +136,6 @@ function handleCancel() {
</div> </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="col-span-2 mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
@@ -159,7 +149,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.addLocation')" :placeholder="$t('entity.calendar.event.addLocation')"
:minlength="3" :minlength="3"
:maxlength="160" :maxlength="160"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500" class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
> >
<div class="mt-2 mb-1 text-xs opacity-50"> <div class="mt-2 mb-1 text-xs opacity-50">
@@ -175,14 +165,9 @@ function handleCancel() {
<PhEyeClosed v-else size="18" /> <PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" /> <UiCheckbox id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility"> <UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden"> {{ $t('entity.calendar.event.hiddenLabel') }}
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel> </UiLabel>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
import { PhCaretDown, PhCheck } from "@phosphor-icons/vue"; import { PhCaretDown, PhCheck } from "@phosphor-icons/vue";
@@ -35,7 +35,9 @@ const filteredCategories = computed(() =>
<UiButton <UiButton
variant="outline" variant="outline"
role="combobox" role="combobox"
class="relative w-full max-w-full h-fit justify-between" :aria-expanded="isPopoverOpen"
aria-controls="event-categories"
class="relative w-full max-w-full h-fit justify-between hover:bg-secondary hover:text-primary-foreground"
> >
<template v-if="!model.length"> <template v-if="!model.length">
{{ props.placeholder }} {{ props.placeholder }}
@@ -50,16 +52,17 @@ const filteredCategories = computed(() =>
</ul> </ul>
</template> </template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <PhCaretDown class="ml-2 size-4 shrink-0 opacity-50" />
</UiButton> </UiButton>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
align="start" id="event-categories"
:align="'start'"
side="bottom" side="bottom"
:collision-padding="50" :collision-padding="50"
class="w-fit h-[33vh] p-0" class="w-fit h-[33vh] p-0"
> >
<UiCommand v-model="modelBuffer" v-model:searchTerm="searchTerm" :multiple="true"> <UiCommand v-model="modelBuffer" v-model:search-term="searchTerm" :multiple="true">
<UiCommandInput :placeholder="$t('entity.category.search')" /> <UiCommandInput :placeholder="$t('entity.category.search')" />
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty> <UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
<UiCommandList> <UiCommandList>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhCaretDown } from "@phosphor-icons/vue"; import { PhCaretDown } from "@phosphor-icons/vue";
import type { Category } from "~/models/Category"; import { cn } from "@/lib/utils";
import type { Category } from "@@/models/Category";
const isPopoverOpen = ref<boolean>(false) const isPopoverOpen = ref<boolean>(false)
@@ -33,25 +34,33 @@ const filteredCategories = computed(() =>
<UiButton <UiButton
variant="outline" variant="outline"
role="combobox" role="combobox"
class="w-full max-w-full justify-between" :aria-expanded="isPopoverOpen"
aria-controls="event-category"
class="w-full max-w-full justify-between hover:bg-secondary hover:text-primary-foreground"
> >
<template v-if="!model"> <template v-if="!model">
{{ props.placeholder }} {{ props.placeholder }}
</template> </template>
<template v-else> <template v-else>
{{ model.name }} <span
class="bgc"
:class="cn(`element-${model.color}`)"
>
{{ capitalize(model.name) }}
</span>
</template> </template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <PhCaretDown class="ml-2 size-4 shrink-0 opacity-50" />
</UiButton> </UiButton>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
id="event-category"
align="start" align="start"
side="bottom" side="bottom"
:collision-padding="50" :collision-padding="50"
class="w-fit h-[33vh] p-0" class="w-fit h-[33vh] p-0"
> >
<UiCommand v-model="model" v-model:searchTerm="searchTerm"> <UiCommand v-model="model" v-model:search-term="searchTerm">
<UiCommandInput :placeholder="$t('entity.category.search')" /> <UiCommandInput :placeholder="$t('entity.category.search')" />
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty> <UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
<UiCommandList> <UiCommandList>
@@ -63,7 +72,12 @@ const filteredCategories = computed(() =>
class="cursor-pointer" class="cursor-pointer"
@select="handleCatSelect" @select="handleCatSelect"
> >
{{ category.name }} <span
class="bgc"
:class="cn(`element-${category.color}`)"
>
{{ capitalize(category.name) }}
</span>
</UiCommandItem> </UiCommandItem>
</UiCommandGroup> </UiCommandGroup>
</UiCommandList> </UiCommandList>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "~/lib/utils"; import { cn } from "@/lib/utils";
import { useSortable } from "@vueuse/integrations/useSortable"; import { useSortable } from "@vueuse/integrations/useSortable";
import type { CalendarMonth } from "~/models/CalendarMonth"; import type { CalendarMonth } from "@@/models/CalendarMonth";
import { PhList, PhPlus, PhTrash } from "@phosphor-icons/vue"; import { PhList, PhPlus, PhTrash } from "@phosphor-icons/vue";
@@ -91,19 +91,19 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
/> />
</div> </div>
<div class="md:col-span-1"> <div class="md:col-span-1">
<UiButton size="icon" class="rounded-full h-8 w-8" :disabled="!validNewMonth" @click.prevent="addMonthToModel"> <UiButton size="icon" class="rounded-full size-8" :disabled="!validNewMonth" @click.prevent="addMonthToModel">
<PhPlus size="17"/> <PhPlus size="17"/>
</UiButton> </UiButton>
</div> </div>
<div class="md:col-span-full"> <div class="md:col-span-full">
<div <div
class="border-[1px] dark:border-slate-800 p-4 rounded-sm max-h-80 overflow-y-auto" 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' : ''" :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"> <div v-if="model.length" class="hidden md:block col-span-1">
<ul class="grid gap-y-4 justify-center"> <ul class="grid gap-y-4 justify-center">
<li v-for="(m, i) in model" :key="`num-${m.name}`"> <li v-for="(m, i) in model" :key="`num-${m.name}`">
<UiButton size="icon" variant="secondary" class="h-8 w-8 rounded-full"> <UiButton size="icon" variant="secondary" class="size-8 rounded-full">
<span class="font-bold text-sm">{{ i + 1 }}</span> <span class="font-bold text-sm">{{ i + 1 }}</span>
</UiButton> </UiButton>
</li> </li>
@@ -115,7 +115,7 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
<template v-if="model.length"> <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"> <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"> <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"> <UiButton type="button" variant="ghost" size="icon" class="handle rounded-full size-8">
<PhList size="17" /> <PhList size="17" />
</UiButton> </UiButton>
</div> </div>
@@ -145,7 +145,7 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
<UiTooltipProvider> <UiTooltipProvider>
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<UiButton type="button" variant="ghost" size="icon" class="rounded-full h-8 w-8" @click="removeMonthFromModel(i)"> <UiButton type="button" variant="ghost" size="icon" class="rounded-full size-8" @click="removeMonthFromModel(i)">
<PhTrash size="17" /> <PhTrash size="17" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>

View File

@@ -1,9 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
import { import {
PhXCircle PhXCircle
} from "@phosphor-icons/vue" } from "@phosphor-icons/vue"
import { cn } from "~/lib/utils";
const model = defineModel<RPGDate | null>() const model = defineModel<RPGDate | null>()
@@ -117,7 +118,6 @@ function handleValueReset() {
v-if="model" v-if="model"
align="start" align="start"
side="bottom" 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"> <div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
<UiSelect @update:model-value="handleMonthChange"> <UiSelect @update:model-value="handleMonthChange">
@@ -135,16 +135,7 @@ function handleValueReset() {
<UiInput v-model="model.year" type="number" /> <UiInput v-model="model.year" type="number" />
<!-- <nav class="flex items-center justify-end gap-2"> <hr class="col-span-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 class="col-span-2">
<div v-if="monthData" class="grid grid-cols-7 gap-1"> <div v-if="monthData" class="grid grid-cols-7 gap-1">
@@ -152,10 +143,10 @@ function handleValueReset() {
v-for="day in monthData.days" v-for="day in monthData.days"
:key="`popover-day-grid-${id}-${day}`" :key="`popover-day-grid-${id}-${day}`"
class="aspect-square rounded-full text-[.8em] transition-colors" class="aspect-square rounded-full text-[.8em] transition-colors"
:class="{ :class="cn({
'hover:bg-indigo-200 dark:hover:bg-indigo-700': day !== model.day, 'hover:bg-accent hover:text-accent-foreground': day !== model.day,
'bg-indigo-500 hover:bg-indigo-700 text-white': day === model.day 'bg-primary text-primary-foreground hover:bg-accent hover:text-accent-foreground': day === model.day
}" })"
@click="setModelDay(day)" @click="setModelDay(day)"
> >
{{ day }} {{ day }}

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CalendarMonth } from "~/models/CalendarMonth"; import type { CalendarMonth } from "@@/models/CalendarMonth";
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
import { PhCalendarBlank } from "@phosphor-icons/vue"; import { PhCalendarBlank } from "@phosphor-icons/vue";

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { breakpointsTailwind } from "@vueuse/core"
const breakpoints = useBreakpoints(
breakpointsTailwind
)
const { isReadOnly, defaultDate } = storeToRefs(useCalendar())
</script>
<template>
<header class="md:mt-2 grid gap-3 md:gap-4 border-border border-b-[1px] transition-colors">
<div class="px-5 md:px-8 flex items-center justify-between gap-2">
<menu class="flex items-center md:gap-2">
<li>
<LazyCalendarDialogQuickCreateEvent v-if="!isReadOnly" />
</li>
<li class="max-md:hidden">
<LazyCalendarMenuToday v-if="defaultDate" />
</li>
<li class="ml-2 md:ml-4">
<CalendarCurrentDate />
</li>
</menu>
<menu class="flex items-center gap-2">
<li>
<CalendarSearchCTA />
</li>
<ClientOnly>
<li v-if="breakpoints.md.value">
<CalendarCategoriesCTA />
</li>
</ClientOnly>
<li>
<CalendarOptionsCTA />
</li>
</menu>
</div>
<div class="ml-4 md:ml-8">
<CalendarMenuSubnav />
</div>
</header>
</template>

View File

@@ -139,7 +139,7 @@ function toFutureFar(): void {
<template> <template>
<div class="flex gap-2"> <div class="flex gap-2">
<div class="grid items-end w-40 px-4 py-2 border-slate-200 bg-white dark:bg-black dark:border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors"> <div class="grid items-center w-40 px-3 md:px-4 py-2 bg-background border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm max-md:text-xs transition-colors">
<ClientOnly> <ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span> <span>{{ currentDate.currentDateTitle }}</span>

View File

@@ -0,0 +1,41 @@
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhCalendar } from "@phosphor-icons/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
})
const breakpoints = useBreakpoints(
breakpointsTailwind
)
</script>
<template>
<ClientOnly>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
:size="breakpoints.md.value ? 'default' : 'icon'"
variant="secondary"
:disabled="buttonDisabledState" @click="jumpToDefaultDate"
>
<PhCalendar v-if="!breakpoints.md.value" size="20" weight="fill" />
<span v-if="breakpoints.md.value">
{{ $t('entity.calendar.date.today') }}
</span>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</ClientOnly>
</template>

View File

@@ -2,13 +2,13 @@
import { import {
isCharacter, isCharacter,
type Character, type Character,
} from "@/models/Characters" } from "@@/models/Characters"
import type { RPGDateOrder } from "@/models/Date" import type { RPGDateOrder } from "@@/models/Date"
import { import {
isCalendarEvent, isCalendarEvent,
type CalendarEvent, type CalendarEvent,
} from "~/models/CalendarEvent" } from "@@/models/CalendarEvent"
import { capitalize } from "@/utils/Strings" import { capitalize } from "~/utils/Strings"
import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core" import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { searchUnifier, type SearchMode } from "../SearchMode" import { searchUnifier, type SearchMode } from "../SearchMode"
@@ -24,7 +24,8 @@ import {
} from "radix-vue" } from "radix-vue"
import SearchList from "./lists/SearchList.vue" import SearchList from "./lists/SearchList.vue"
import type { Category } from "~/models/Category" import type { Category } from "@@/models/Category"
import { cn } from "@/lib/utils"
const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar()) const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar())
const { characters } = storeToRefs(useCharacters()) const { characters } = storeToRefs(useCharacters())
@@ -87,7 +88,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
results = dataToFilter.filter((item) => { results = dataToFilter.filter((item) => {
// Filter calendar events // Filter calendar events
if (isCalendarEvent(item)) { if (isCalendarEvent(item)) {
const queryString = new String(searchQuery.value) const queryString = (searchQuery.value as string)
.replace(searchUnifier, "") .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
@@ -125,7 +126,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Filter characters // Filter characters
if (isCharacter(item)) { if (isCharacter(item)) {
const queryString = new String(searchQuery.value) const queryString = (searchQuery.value as string)
.replace(searchUnifier, "") .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
@@ -194,10 +195,9 @@ function handleEntitySwitch() {
// Key combos to deploy modal // Key combos to deploy modal
const keys = useMagicKeys() const keys = useMagicKeys()
const controlPeriod = computed(() => keys.control_period?.value)
whenever(keys.control_period, () => { whenever(controlPeriod, openUiDialog)
openUiDialog()
})
const searchResultsRef = ref<HTMLElement | null>(null) const searchResultsRef = ref<HTMLElement | null>(null)
const { y: searchResultsY } = useScroll(searchResultsRef) const { y: searchResultsY } = useScroll(searchResultsRef)
@@ -264,10 +264,10 @@ function handleCategoryUnselect(e: Category) {
<template> <template>
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()"> <UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
<UiDialogContent <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="flex flex-col flex-nowrap top-10 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%] max-md:rounded-none"
:class="{ :class="cn({
'bottom-16': searchResults.length > 0 'max-md:w-full bottom-0 md:bottom-10 top-0 md:top-10': searchResults.length > 0
}" })"
> >
<VisuallyHidden> <VisuallyHidden>
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle> <UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
@@ -278,7 +278,6 @@ function handleCategoryUnselect(e: Category) {
</UiDialogDescription> </UiDialogDescription>
</VisuallyHidden> </VisuallyHidden>
<!-- UiDialog header -->
<div id="searchForm" class="grid gap-3"> <div id="searchForm" class="grid gap-3">
<div class="relative w-full h-fit"> <div class="relative w-full h-fit">
<UiInput <UiInput
@@ -296,7 +295,7 @@ function handleCategoryUnselect(e: Category) {
</div> </div>
<div class="flex items-center justify-between gap-8"> <div class="flex items-center justify-between gap-8">
<div> <div class="hidden">
<UiToggleGroup <UiToggleGroup
v-model="selectedEntity" v-model="selectedEntity"
type="single" type="single"
@@ -306,18 +305,20 @@ function handleCategoryUnselect(e: Category) {
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements"> <UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
{{ $t('entity.calendar.event.namePlural') }} {{ $t('entity.calendar.event.namePlural') }}
</UiToggleGroupItem> </UiToggleGroupItem>
<!-- Not used for now -->
<!-- <UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
{{ $t('entity.character.namePlural') }}
</UiToggleGroupItem> -->
</UiToggleGroup> </UiToggleGroup>
</div> </div>
<div class="grow flex justify-end items-center gap-1"> <div class="grow flex justify-end items-center gap-1">
<UiTagsInput class="grow px-0 gap-y-1 w-80"> <UiTagsInput class="grow px-0 gap-y-1 md:w-80">
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3"> <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"> <UiTagsInputItem v-for="cat in selectedCategories" :key="cat.id" :value="cat.name">
<UiTagsInputItemText class="capitalize cursor-pointer" @click="handleCategoryUnselect(item)" /> <button
class="bgc px-2 capitalize cursor-pointer"
:class="cn(`element-${cat.color}`)"
@click="handleCategoryUnselect(cat)"
>
{{ capitalize(cat.name) }}
</button>
</UiTagsInputItem> </UiTagsInputItem>
</div> </div>
@@ -345,7 +346,7 @@ function handleCategoryUnselect(e: Category) {
<ComboboxPortal :to="'#searchForm'"> <ComboboxPortal :to="'#searchForm'">
<UiCommandList <UiCommandList
position="popper" position="popper"
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none 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" 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" :dismissable="true"
> >
<UiCommandEmpty /> <UiCommandEmpty />
@@ -356,7 +357,12 @@ function handleCategoryUnselect(e: Category) {
:value="cat" :value="cat"
@select.prevent="handleCategorySelect(cat)" @select.prevent="handleCategorySelect(cat)"
> >
{{ capitalize(cat.name) }} <span
class="bgc"
:class="cn(`element-${cat.color}`)"
>
{{ capitalize(cat.name) }}
</span>
</UiCommandItem> </UiCommandItem>
</UiCommandGroup> </UiCommandGroup>
</UiCommandList> </UiCommandList>
@@ -401,7 +407,7 @@ function handleCategoryUnselect(e: Category) {
</div> </div>
</div> </div>
<hr > <hr>
<div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto"> <div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto">
<SearchList <SearchList
@@ -435,7 +441,7 @@ function handleCategoryUnselect(e: Category) {
as-child as-child
> >
<UiButton <UiButton
class="w-10 h-10 p-0" class="size-8 md:size-10 p-0"
:variant="item.value === currentPage ? 'default' : 'outline'" :variant="item.value === currentPage ? 'default' : 'outline'"
> >
{{ item.value }} {{ item.value }}

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Character } from "@/models/Characters" import type { Character } from "@@/models/Characters"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"
import { Button } from "@/components/ui/button" import { Button } from "~/components/ui/button"
import { PhArrowSquareOut, PhPlant, PhSkull } from "@phosphor-icons/vue" import { PhArrowSquareOut, PhPlant, PhSkull } from "@phosphor-icons/vue"
const props = defineProps<{ const props = defineProps<{
@@ -12,7 +12,7 @@ const props = defineProps<{
}>() }>()
defineEmits<{ defineEmits<{
(e: "query:date-jump", payload: RPGDate): void "query:date-jump": [payload: RPGDate]
}>() }>()
const { getFormattedDateTitle } = useCalendar() const { getFormattedDateTitle } = useCalendar()
@@ -21,7 +21,7 @@ console.log(props.character.birth)
</script> </script>
<template> <template>
<div class="block w-full text-left py-3 px-4 border-[1px] border-slate-700 rounded-sm"> <div class="block w-full text-left py-3 px-4 border-[1px] border-border rounded-sm">
<div class="grid gap-2"> <div class="grid gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<h2 class="font-bold"> <h2 class="font-bold">
@@ -37,7 +37,7 @@ console.log(props.character.birth)
</div> </div>
</div> </div>
<menu class="flex gap-2 border-[1px] border-slate-700 rounded-sm w-fit"> <menu class="flex gap-2 border-[1px] border-border rounded-sm w-fit">
<li v-if="character.birth"> <li v-if="character.birth">
<TooltipProvider :delay-duration="100"> <TooltipProvider :delay-duration="100">
<Tooltip> <Tooltip>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue" import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue"
@@ -10,7 +10,7 @@ const props = defineProps<{
}>() }>()
defineEmits<{ defineEmits<{
(e: "query:date-jump", payload: RPGDate): void "query:date-jump": [payload: RPGDate]
}>() }>()
const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar() const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar()
@@ -21,16 +21,16 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
<template> <template>
<button <button
class="event group relative block w-full text-left py-3 px-4 rounded-sm transition-colors" class="event-callout group relative block w-full text-left py-2 px-3 md:py-3 md:px-4 cursor-pointer transition-colors"
:class="cn( :class="cn(
event.category ? `event-${event.category.color}` : '', event.category ? `element-${event.category.color}` : '',
{ {
'pt-4 is-hidden': event.hidden, 'pt-4 is-hidden': event.hidden,
})" })"
@click="$emit('query:date-jump', event.startDate)" @click="$emit('query:date-jump', event.startDate)"
> >
<div class="flex gap-2 items-center mb-1"> <div class="flex gap-2 items-center mb-1">
<h2 class="font-bold text-lg underline-offset-4 group-hover:underline"> <h2 class="font-semibold md:font-bold md:text-lg underline-offset-4 group-hover:underline">
{{ event.title }} {{ event.title }}
</h2> </h2>
<div v-if="event.wiki"> <div v-if="event.wiki">
@@ -43,7 +43,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
</div> </div>
</div> </div>
<div class="flex gap-2 items-center justify-between mb-1"> <div class="flex gap-2 items-center justify-between mb-2 md:mb-1">
<template v-if="!event.endDate"> <template v-if="!event.endDate">
<p class="font-semibold text-sm opacity-75"> <p class="font-semibold text-sm opacity-75">
{{ getFormattedDateTitle(event.startDate, true) }} {{ getFormattedDateTitle(event.startDate, true) }}
@@ -81,15 +81,15 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
<div class="mb-1 flex gap-x-2 items-center"> <div class="mb-1 flex gap-x-2 items-center">
<template v-if="event.location"> <template v-if="event.location">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
<PhMapPinArea size="16" weight="fill" /> {{ event.location }} <PhMapPinArea size="16" weight="fill" /> {{ event.location }}
</p> </p>
</template> </template>
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }} <PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p> </p>
<template v-if="dateDuration"> <template v-if="dateDuration">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}} <PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
</p> </p>
</template> </template>
@@ -111,11 +111,9 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
</ul> </ul>
</div> </div>
<div v-if="event.description" class="text-sm"> <div v-if="event.description" class="text-xs md:text-sm">
<hr class="my-2 border-white opacity-50" > <hr class="my-3 md:my-2 border-white opacity-50" >
<span class="opacity-75"> <div class="content-editor opacity-75" v-html="event.description" />
{{ event.description }}
</span>
</div> </div>
</button> </button>
</template> </template>

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isCharacter, type Character } from "@/models/Characters" import { isCharacter, type Character } from "@@/models/Characters"
import type { RPGDate, RPGDateOrder } from "@/models/Date" import type { RPGDate, RPGDateOrder } from "@@/models/Date"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { computed } from "vue" import { computed } from "vue"
import { isCalendarEvent, type CalendarEvent } from "~/models/CalendarEvent" import { isCalendarEvent, type CalendarEvent } from "@@/models/CalendarEvent"
import type { SearchMode } from "../../SearchMode" import type { SearchMode } from "../../SearchMode"
import CharacterCallout from "./CharacterCallout.vue" import CharacterCallout from "./CharacterCallout.vue"
@@ -60,7 +60,7 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
</script> </script>
<template> <template>
<ul class="grid gap-4 pl-8 pr-12"> <ul class="grid gap-3 md:gap-4 pl-4 pr-6 md:pl-8 md:pr-12">
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name"> <li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" /> <EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />

View File

@@ -0,0 +1,264 @@
<script lang="ts" setup>
import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "@@/models/CalendarEvent"
import { breakpointsTailwind, useElementBounding } from "@vueuse/core"
import { storeToRefs } from "pinia"
import { computed, ref, type ComputedRef } from "vue"
import CalendarEventButton from "../../event/Event.vue"
import { useToast } from "@/components/ui/toast"
import type { APIError } from "@@/models/Errors"
import { ToastLifetime } from "@/components/ui/toast/use-toast"
const props = defineProps<{
date: RPGDate
faded?: boolean
}>()
const emit = defineEmits<{
"on-open-create-dialog": [date: RPGDate]
}>()
const { toast } = useToast()
const { t } = useI18n()
const calendarTile = ref()
const calendarEventsList = ref()
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle, updateEventFromSkeleton, resetSkeleton } = useCalendar()
const { selectedDate, currentEvents, isReadOnly, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
const breakpoints = useBreakpoints(
breakpointsTailwind
)
/**
* 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
let offset = 0
if (isDraggingEvent.value && !tileNotHovered.value && !skeletonInsideCurrentDay.value) {
offset = 1
}
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40) - offset
})
const skeletonInsideCurrentDay = computed(() => eventsForTheDay.value.find((e) => e.id === eventSkeleton.value.id))
/**
* 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)
// Handle drop events
const { isOutside: tileNotHovered } = useMouseInElement(calendarTile)
const isLoading = ref(false)
async function handleTileDrop() {
if (isLoading.value) return
if (skeletonInsideCurrentDay.value) return
isLoading.value = true
eventSkeleton.value.startDate = props.date
const { error } = await tryCatch(updateEventFromSkeleton())
if (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const apiError = (error 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,
})
})
isLoading.value = false
return
}
isLoading.value = false
resetSkeleton()
}
</script>
<template>
<div
ref="calendarTile"
class="tile relative p-1 md:p-2 md:pt-1 transition-all"
:class="{
'text-slate-300 dark:text-slate-500': props.faded,
'text-slate-500 dark:text-slate-300': !props.faded
}"
@drop.prevent="handleTileDrop"
@dragenter.prevent
@dragover.prevent
>
<button
class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer"
@click="selectDate(date)"
>
<ClientOnly>
<span
class="inline-flex size-7 md:size-8 aspect-square items-center justify-center rounded-full border-1 md:border-2 border-transparent font-bold transition-colors group-hover:border-primary dark:group-hover:border-primary"
:class="{
'bg-secondary dark:bg-slate-700': isDefaultDate && !isSelectedDate,
'bg-primary text-primary-foreground': isSelectedDate
}"
>
{{ date.day }}
</span>
</ClientOnly>
</button>
<ClientOnly>
<ul
ref="calendarEventsList"
class="absolute top-8 md:top-10 bottom-1 md:bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
:class="{
'opacity-40': props.faded && !isSelectedDate
}"
>
<li
v-if="!tileNotHovered && isDraggingEvent && !skeletonInsideCurrentDay"
:key="eventSkeleton.id"
class="opacity-60"
>
<CalendarEventButton :event="eventSkeleton" :tile-date="date" />
</li>
<TransitionGroup :name="!isDraggingEvent ? 'event' : ''">
<li
v-for="(event, i) 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-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:text-foreground hover:bg-foreground/10"
>
<template v-if="eventsNotDisplayed === 1">
{{ $t('entity.calendar.event.oneOtherEvent') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.multipleOtherEvents', { count: eventsNotDisplayed }) }}
</template>
</button>
</UiPopoverTrigger>
<UiPopoverContent
class="w-80"
:align="'center'"
:align-offset="breakpoints.lg.value ? 50 : 25"
:collision-padding="breakpoints.lg.value ? 50 : 25"
:side="breakpoints.lg.value ? 'left' : 'bottom'"
>
<div class="text-center mb-2">
<span
class="text-lg font-semibold rounded-full"
>
{{ getFormattedDateTitle(date, true) }}
</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 && breakpoints.lg.value" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
<button
v-else-if="!isReadOnly && !breakpoints.lg.value"
class="absolute inset-0 w-full h-full cursor-default z-0"
@click="emit('on-open-create-dialog', props.date)"
/>
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
.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

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from "@vueuse/core"
import type { RPGDate } from "@@/models/Date"
import { VisuallyHidden } from "radix-vue"
const { currentDate, decrementViewMonth, incrementViewMonth, resetSkeleton } = useCalendar()
const { currentMonthData, operationInProgress, eventSkeleton } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
// Prevent scrolling
e.preventDefault()
const isMovingUp = e.deltaY < 0
if (isMovingUp) {
moveCalendarLeft()
} else {
moveCalendarRight()
}
}
const moveCalendarLeft = useThrottleFn(() => {
decrementViewMonth()
}, 100)
const moveCalendarRight = useThrottleFn(() => {
incrementViewMonth()
}, 100)
const isDialogOpen = ref<boolean>(false);
/**
* Opens event creation's popover
*/
function handleDialogOpen(date: RPGDate) {
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
if (operationInProgress.value) {
isDialogOpen.value = false
return
}
resetSkeleton()
isDialogOpen.value = true
// Set skeleton initial startDate if it's known
if (date) {
eventSkeleton.value.startDate = date
}
}
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
/**
* Prevents the modal from closing if's still loading
*/
function handleClosing() {
setTimeout(() => resetSkeleton(), 100)
}
</script>
<template>
<div class="grid grid-cols-5 md:grid-cols-7 lg: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
}"
@on-open-create-dialog="handleDialogOpen"
/>
<UiDialog v-model:open="isDialogOpen">
<UiDialogContent
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
:trap-focus="true"
@escape-key-down="handleClosing"
@pointer-down-outside.prevent="null"
>
<UiDialogTitle class="max-md:mb-8">
{{ $t("entity.calendar.event.addSingle") }}
</UiDialogTitle>
<VisuallyHidden>
<UiDialogDescription>
{{ $t("entity.calendar.event.addSingleDescription") }}
</UiDialogDescription>
</VisuallyHidden>
<CalendarFormCreateEvent @event-created="toggleDialog" />
</UiDialogContent>
</UiDialog>
</template>
</div>
</template>

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia"
import { computed, type ComputedRef } from "vue" import { computed, type ComputedRef } from "vue"
import { cn } from "~/lib/utils"
const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar() const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate, currentEvents } = storeToRefs(useCalendar()) const { selectedDate, currentEvents } = storeToRefs(useCalendar())
@@ -38,14 +39,14 @@ const hasAtLeastOneEvent = computed<boolean>(() => {
<template> <template>
<button <button
class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors" class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors cursor-pointer"
:class="{ :class="cn({
'text-slate-500 hover:border-indigo-300 hover:text-slate-900': !isDefaultDate && !isSelectedDate, 'text-foreground/75 hover:border-primary hover:text-foreground': !isDefaultDate && !isSelectedDate,
'font-semibold text-white bg-slate-600 dark:bg-slate-800 hover:border-indigo-400': isDefaultDate && !isSelectedDate, 'font-semibold text-foreground bg-secondary hover:border-primary/70': isDefaultDate && !isSelectedDate,
'font-semibold text-white bg-indigo-500 hover:bg-indigo-600 hover:border-indigo-500': isSelectedDate, 'font-semibold text-primary-foreground bg-primary hover:border-primary/70': isSelectedDate,
'after:bg-green-600': hasAtLeastOneEvent, 'after:bg-primary': hasAtLeastOneEvent,
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate 'after:bg-primary-foreground': hasAtLeastOneEvent && isSelectedDate
}" })"
@click="selectDate(tileDate)" @click="selectDate(tileDate)"
> >
<span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span> <span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span>

View File

@@ -24,7 +24,7 @@ const moveCalendarRight = useThrottleFn(() => {
</script> </script>
<template> <template>
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel"> <div class="container mx-auto mt-[10vh] mb-auto" @wheel="handleWheel">
<div class="grid grid-cols-5 gap-x-8 gap-y-16"> <div class="grid grid-cols-5 gap-x-8 gap-y-16">
<CalendarStateYearlyMonthTile <CalendarStateYearlyMonthTile
v-for="month in months" v-for="month in months"

View File

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

View File

@@ -0,0 +1,66 @@
<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>
<div class="flex gap-3 items-center">
<div class="md:hidden">
<SidebarToggle />
</div>
<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>
</div>
</template>

View File

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

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "~/lib/utils"; import { cn } from "@/lib/utils";
interface SpacingProps { interface SpacingProps {
size?: "xs" | "sm" | "md" | "lg" | "xlg" | "2xl" | "3xl" size?: "xs" | "sm" | "md" | "lg" | "xlg" | "2xl" | "3xl"

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { PhCloudX } from '@phosphor-icons/vue';
</script>
<template>
<UiCard class="w-full h-full flex flex-col border-rose-200 bg-rose-50 dark:border-rose-900 dark:bg-rose-950">
<template v-if="$slots.title">
<UiCardHeader class="flex-row items-center gap-2">
<PhCloudX :size="32"/>
<UiCardTitle>
<slot name="title" />
</UiCardTitle>
</UiCardHeader>
</template>
<template v-if="$slots.content">
<UiCardContent class="grow grid gap-2">
<slot name="content" />
</UiCardContent>
</template>
</UiCard>
</template>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { type RPGColor, rpgColors } from "~/models/Color"; import { type RPGColor, rpgColors } from "@@/models/Color";
const { id, theme = "normal", position = "popper" } = defineProps<{ const { id, theme = "normal", position = "popper" } = defineProps<{
id: string id: string
@@ -13,11 +13,19 @@ const model = defineModel<RPGColor>({ default: "white" });
<template> <template>
<UiSelect v-model="model"> <UiSelect v-model="model">
<UiSelectTrigger :id :class="cn({ 'h-9': theme === 'subtle' })"> <UiSelectTrigger
:id
:class="cn(
['hover:bg-secondary hover:text-primary-foreground'],
{
'h-9': theme === 'subtle'
}
)"
>
<UiSelectValue <UiSelectValue
:placeholder="$t('ui.colors.selectOne')" :placeholder="$t('ui.colors.selectOne')"
class="bgc" class="bgc"
:class="cn(`bgc-${model}`)" :class="cn(`element-${model}`)"
/> />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent :position> <UiSelectContent :position>
@@ -29,7 +37,7 @@ const model = defineModel<RPGColor>({ default: "white" });
class="bgc" class="bgc"
:class=" :class="
cn( cn(
`bgc-${color}`, `element-${color}`,
) )
" "
> >

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { contentStates, type ContentState } from "~/models/Entity"; import { contentStates, type ContentState } from "@@/models/Entity";
defineProps<{ defineProps<{
id: string id: string

View File

@@ -0,0 +1,343 @@
<script lang="ts" setup>
import { useEditor, EditorContent, type Content } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { PhArrowUDownRight, PhArrowUUpLeft, PhListBullets, PhListNumbers, PhQuotes, PhTextB, PhTextItalic, PhTextStrikethrough, PhTextTSlash } from '@phosphor-icons/vue'
const props = defineProps<{
disableInlines?: boolean
disableBlocks?: boolean
}>()
const model = defineModel<any>()
const editor = useEditor({
content: model.value as Content,
extensions: [
StarterKit
],
onUpdate: ({ editor }) => {
model.value = editor.getHTML()
}
})
function clearFormatting() {
editor.value?.chain().focus().unsetAllMarks().run()
editor.value?.chain().focus().clearNodes().run()
}
</script>
<template>
<div v-if="editor">
<!-- Toolbar -->
<div class="flex gap-1.5">
<!-- Inline styles -->
<div class="flex" v-if="!props.disableInlines">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:variant="editor.isActive('bold') ? 'default' : 'secondary'"
class="size-7 rounded-e-none"
size="icon"
>
<PhTextB weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.bold') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:variant="editor.isActive('italic') ? 'default' : 'secondary'"
class="size-7 rounded-none"
size="icon"
>
<PhTextItalic weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.italic') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:variant="editor.isActive('strike') ? 'default' : 'secondary'"
class="size-7 rounded-s-none"
size="icon"
>
<PhTextStrikethrough weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.strike') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<!-- Block styles -->
<div class="flex" v-if="!props.disableBlocks">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleBulletList().run()"
:variant="editor.isActive('bulletList') ? 'default' : 'secondary'"
class="size-7 rounded-e-none"
size="icon"
>
<PhListBullets weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.ulist') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleOrderedList().run()"
:variant="editor.isActive('orderedList') ? 'default' : 'secondary'"
class="size-7 rounded-none"
size="icon"
>
<PhListNumbers weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.olist') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleBlockquote().run()"
:variant="editor.isActive('blockquote') ? 'default' : 'secondary'"
class="size-7 rounded-s-none"
size="icon"
>
<PhQuotes weight="fill" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.quote') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div class="flex">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="clearFormatting"
variant="secondary"
class="size-7 rounded-e-none"
size="icon"
>
<PhTextTSlash weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.noFormat') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
variant="secondary"
class="size-7 rounded-none"
size="icon"
>
<PhArrowUUpLeft weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.undo') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
variant="secondary"
class="size-7 rounded-s-none"
size="icon"
>
<PhArrowUDownRight weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.redo') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
<EditorContent
:editor
class="overflow-y-auto md:text-sm border-b-1 border-border"
/>
</div>
</template>
<style lang="scss">
.tiptap,
.content-editor {
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
}
/* Basic editor styles */
.tiptap {
padding-block: 0.75rem;
min-height: 7rem;
max-height: 12rem;
overflow-y: auto;
:first-child {
margin-top: 0;
}
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { PanelsLeftBottom } from "lucide-vue-next"
import { cn } from "@/lib/utils";
import { PhInfo } from "@phosphor-icons/vue";
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template>
<nav
ref="sidebarRef"
:class="cn(
['max-md:hidden w-fit relative isolate p-2 grid gap-4 grid-rows-[1fr_auto] bg-background justify-center transition-colors'], // Base appearance
['after:opacity-50 after:contrast-125 dark:after:opacity-75'], // After styling
['border-r-[1px] border-r-border dark:border-r-border'], // Colours
)"
>
<SidebarMenu />
<SidebarFooter />
</nav>
<UiSheet v-model:open="isSidebarOpened">
<UiSheetContent side="left" class="p-2 grid gap-4 grid-rows-[auto_1fr]">
<UiSheetHeader />
<SidebarMenu />
<SidebarFooter />
</UiSheetContent>
</UiSheet>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { PhList } from "@phosphor-icons/vue"
const { toggleSidebar } = useUiStore()
</script>
<template>
<UiButton
size="icon"
variant="outline"
class="size-7 border-border hover:bg-secondary hover:text-secondary-foreground"
@click="toggleSidebar"
>
<PhList size="14" weight="light" />
</UiButton>
</template>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue" import { computed } from "vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue" import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle, PhInfo } from "@phosphor-icons/vue"
import { cn } from "~/lib/utils"; import { cn } from "@/lib/utils";
const router = useRouter() const router = useRouter()
@@ -21,57 +21,65 @@ function closeMenu() {
watch(user, closeMenu) watch(user, closeMenu)
async function handleGoogleLogin() { async function handleGoogleLogin() {
try { const { error } = await auth.signInWithOAuth({
auth.signInWithOAuth({ provider: "google",
provider: "google", options: {
options: { queryParams: {
queryParams: { access_type: "offline",
access_type: "offline", prompt: "consent"
prompt: "consent" },
}, redirectTo: profileUrl
redirectTo: profileUrl }
} })
})
} catch (err) { if (error) {
console.log(err) console.log(error.message)
} }
} }
async function handleLogout() { async function handleLogout() {
try { const { error } = await auth.signOut()
const { error } = await auth.signOut()
if (error) throw error if (error) {
} catch (err) { console.log(error.message)
console.log(err)
} }
} }
type AvailableRoutes = "/my" | "/my/settings"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script> </script>
<template> <template>
<ClientOnly> <ClientOnly>
<UiDropdownMenu v-model:open="menuOpened"> <UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger> <UiDropdownMenuTrigger as-child>
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer"> <UiButton
<UiAvatarImage variant="ghost"
:src="userMeta?.avatar_url" class="w-full justify-start p-2 h-fit hover:text-secondary-foreground hover:bg-secondary"
:alt="userMeta?.full_name" :class="cn({ 'bg-secondary': menuOpened })"
referrerpolicy="no-referrer" >
/> <TransitionGroup name="fade-group" appear>
<UiAvatarFallback> <div
{{ $t('ui.sidebarMenu.avatarFallback') }} v-if="user"
</UiAvatarFallback> class="flex gap-2 items-center"
</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"> <UiAvatar
<PhUserCircle size="24" /> id="user-avatar"
size="sm"
class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
>
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<div class="text-xs">
{{ userMeta?.name }}
</div>
</div>
</TransitionGroup>
</UiButton> </UiButton>
</UiDropdownMenuTrigger> </UiDropdownMenuTrigger>
@@ -80,15 +88,6 @@ function pushRoute(to: AvailableRoutes) {
<p class="p-2 text-[.7em] opacity-75"> <p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.greeting', { user: user?.email }) }} {{ $t('ui.greeting', { user: user?.email }) }}
</p> </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>
<template v-else> <template v-else>
@@ -186,12 +185,6 @@ function pushRoute(to: AvailableRoutes) {
<UiDropdownMenuSeparator /> <UiDropdownMenuSeparator />
<template v-if="user"> <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"> <UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="handleLogout">
<PhSignOut size="20" weight="fill" /> <PhSignOut size="20" weight="fill" />
<span> <span>

View File

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

View File

@@ -2,7 +2,7 @@
import { type HTMLAttributes, computed } from "vue" import { type HTMLAttributes, computed } from "vue"
import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue" import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button/index" import { buttonVariants } from "~/components/ui/button/index"
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>() const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>()

View File

@@ -2,7 +2,7 @@
import { type HTMLAttributes, computed } from "vue" import { type HTMLAttributes, computed } from "vue"
import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue" import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button/index" import { buttonVariants } from "~/components/ui/button/index"
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>() const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>()

View File

@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( 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-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', 'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-popover-border bg-popover 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% rounded-lg',
props.class, props.class,
) )
" "

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue" export { default as Badge } from "@/components/ui/badge/Badge.vue"
export const badgeVariants = cva( 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-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "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: { variants: {
variant: { variant: {

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { MoreHorizontal } from "lucide-vue-next" import { PhArrowsHorizontal } from "@phosphor-icons/vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@@ -15,7 +15,7 @@ const props = defineProps<{
:class="cn('flex h-9 w-9 items-center justify-center', props.class)" :class="cn('flex h-9 w-9 items-center justify-center', props.class)"
> >
<slot> <slot>
<MoreHorizontal class="h-4 w-4" /> <PhArrowsHorizontal class="size-4" />
</slot> </slot>
<span class="sr-only">More</span> <span class="sr-only">More</span>
</span> </span>

View File

@@ -13,7 +13,7 @@ const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes
<Primitive <Primitive
:as="as" :as="as"
:as-child="asChild" :as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)" :class="cn('transition-colors rounded-[1px] hover:text-primary focus-visible:text-primary underline-offset-4 hover:underline focus-visible:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-4', props.class)"
> >
<slot /> <slot />
</Primitive> </Primitive>

View File

@@ -9,7 +9,7 @@ const props = defineProps<{
<template> <template>
<ol <ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)" :class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground transition-colors sm:gap-2.5', props.class)"
> >
<slot /> <slot />
</ol> </ol>

View File

@@ -12,7 +12,7 @@ const props = defineProps<{
role="link" role="link"
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
:class="cn('font-normal text-foreground', props.class)" :class="cn('font-normal text-foreground transition-colors', props.class)"
> >
<slot /> <slot />
</span> </span>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronRight } from "lucide-vue-next" import { PhCaretRight } from "@phosphor-icons/vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@@ -15,7 +15,7 @@ const props = defineProps<{
:class="cn('[&>svg]:w-3 [&>svg]:h-3', props.class)" :class="cn('[&>svg]:w-3 [&>svg]:h-3', props.class)"
> >
<slot> <slot>
<ChevronRight /> <PhCaretRight />
</slot> </slot>
</li> </li>
</template> </template>

View File

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

View File

@@ -20,13 +20,13 @@ const props = withDefaults(defineProps<Props>(), {
<Primitive <Primitive
:as="as" :as="as"
:as-child="asChild" :as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class, 'group')" :class="cn(buttonVariants({ variant, size }), props.class, 'group cursor-pointer')"
> >
<slot /> <slot />
<span <span
v-if="props.searchSlash" v-if="props.searchSlash"
class="h-4 p-1 ml-1 grid place-items-center text-[0.7em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-sm group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors" class="h-4 p-1 ml-1 grid place-items-center text-[0.7em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-xs group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors"
> >
<kbd class="-mt-[2px]">CTRL + :</kbd> <kbd class="-mt-[2px]">CTRL + :</kbd>
</span> </span>

View File

@@ -1,17 +1,17 @@
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
export { default as Button } from "./Button.vue" export { default as Button } from "@/components/ui/button/Button.vue"
export const buttonVariants = cva( 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-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30", "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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/70",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground", outline: "border border-input hover:bg-primary hover:text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/20", secondary: "bg-secondary text-secondary-foreground hover:bg-primary/40",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-primary/70 hover:text-primary-foreground",
link: "text-primary underline-offset-4 hover:underline" link: "text-primary underline-offset-4 hover:underline"
}, },
size: { size: {

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