Compare commits
129 Commits
features/c
...
feat/nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
674a38c30d | ||
|
|
e7cef314a1 | ||
|
|
806d3a7908 | ||
|
|
6251499940 | ||
|
|
f3d5db76ac | ||
|
|
08620b5044 | ||
|
|
f89cc22e87 | ||
|
|
51c678d27e | ||
|
|
b9b891c61f | ||
|
|
df06228e68 | ||
|
|
44193aa018 | ||
|
|
73eceda96c | ||
|
|
e305b21f80 | ||
|
|
d2b66a42dd | ||
|
|
be32a018c6 | ||
|
|
a5a232bac7 | ||
|
|
20045b125e | ||
|
|
f1a3854d1d | ||
|
|
cebcb50903 | ||
|
|
396215bb91 | ||
|
|
7468374bb5 | ||
|
|
5a421cf07f | ||
|
|
25a9099a18 | ||
|
|
c208393f9f | ||
|
|
d75f3b5423 | ||
|
|
97ce5b3553 | ||
|
|
0a35f441e0 | ||
|
|
c8a345406c | ||
|
|
119968e7fc | ||
|
|
9b8f48ad8e | ||
|
|
2199359b96 | ||
|
|
422673cc4c | ||
|
|
6aad01c96f | ||
|
|
0c96466263 | ||
|
|
7697e818bf | ||
|
|
fd766b9f42 | ||
|
|
0e7b60235a | ||
|
|
300507bbed | ||
|
|
693f506610 | ||
|
|
f2076e413c | ||
|
|
6b02629b01 | ||
|
|
1d8b56c58e | ||
|
|
8290d02900 | ||
|
|
b67f663f05 | ||
|
|
456135d679 | ||
|
|
7fdab8601f | ||
|
|
68b9a38f83 | ||
|
|
77752e8164 | ||
|
|
1d74d6f64b | ||
|
|
b0975ee293 | ||
|
|
33f4f78ea7 | ||
|
|
4a04728183 | ||
|
|
8cd20aa386 | ||
|
|
8e325196e6 | ||
|
|
47883d1577 | ||
|
|
4433f5f1b6 | ||
|
|
f132e6d1e0 | ||
|
|
245fb421b3 | ||
|
|
46d7aa74f9 | ||
|
|
9e40ab0982 | ||
|
|
63d5ba2224 | ||
|
|
114e235a93 | ||
|
|
f03d952183 | ||
|
|
45eefbe35d | ||
|
|
18f332be54 | ||
|
|
67b79000f6 | ||
|
|
896bac5f83 | ||
|
|
09cd25ce33 | ||
|
|
1cf8510a7b | ||
|
|
a83096a17b | ||
|
|
216617a8e6 | ||
|
|
a668fa8d81 | ||
|
|
0fdb1508a4 | ||
|
|
d7447eb700 | ||
|
|
c2712f7b86 | ||
|
|
6b01ce181d | ||
|
|
acb5c0d92f | ||
|
|
387597755d | ||
|
|
a5f7e1ded9 | ||
|
|
93a03ca4e2 | ||
|
|
a6fe4c7ea3 | ||
|
|
805972bf3c | ||
|
|
c071e9c33b | ||
|
|
b430ab2660 | ||
|
|
a6d388f5da | ||
|
|
a03b7890e6 | ||
|
|
b71e715cf5 | ||
|
|
9ba09c2d2b | ||
|
|
b1ff2ec232 | ||
|
|
093b4e7493 | ||
|
|
7b0bb1e939 | ||
|
|
60bbb6f48f | ||
|
|
f502f80341 | ||
|
|
e634cc8127 | ||
|
|
ee29169f4f | ||
|
|
2d2069cae6 | ||
|
|
f7f12feaec | ||
|
|
5b8fedba7c | ||
|
|
fa872e6914 | ||
|
|
43dd12782b | ||
|
|
f1f140f2bc | ||
|
|
707d4374ef | ||
|
|
048413286f | ||
|
|
9d06040c5d | ||
|
|
15bfc66087 | ||
|
|
e6b2670438 | ||
|
|
e2d2810db6 | ||
|
|
1827b9118f | ||
|
|
086b997845 | ||
|
|
73f5e01244 | ||
|
|
d02bfacd57 | ||
|
|
5370a9df06 | ||
|
|
5bd76495f4 | ||
|
|
7480447420 | ||
|
|
034713961b | ||
|
|
00224417be | ||
|
|
9b4556245e | ||
|
|
ad8120bdd0 | ||
|
|
73bd79c3c5 | ||
|
|
8215791830 | ||
|
|
36456ce377 | ||
|
|
015d0c4db4 | ||
|
|
392d8cd8ec | ||
|
|
473c69c9a1 | ||
|
|
790deeec5d | ||
|
|
687b96cf12 | ||
|
|
3e973dd0e9 | ||
|
|
cc07f898ea | ||
|
|
e5bc5c8a41 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,3 +26,5 @@ logs
|
|||||||
# Supabase
|
# Supabase
|
||||||
.branches
|
.branches
|
||||||
.temp
|
.temp
|
||||||
|
|
||||||
|
.scannerwork/
|
||||||
|
|||||||
@@ -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
331
app/assets/_colors.css
Normal 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
66
app/assets/_content.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/assets/_typography.css
Normal file
10
app/assets/_typography.css
Normal 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
63
app/assets/main.css
Normal 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
111
app/assets/theme.css
Normal 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);
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
25
app/components/calendar/CategoriesCTA.vue
Normal file
25
app/components/calendar/CategoriesCTA.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
103
app/components/calendar/PreviewCard.vue
Normal file
103
app/components/calendar/PreviewCard.vue
Normal 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>
|
||||||
28
app/components/calendar/SearchCTA.vue
Normal file
28
app/components/calendar/SearchCTA.vue
Normal 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>
|
||||||
@@ -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
|
||||||
@@ -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[]
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
@@ -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') }}
|
||||||
@@ -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"
|
||||||
70
app/components/calendar/dialog/QuickCreateEvent.vue
Normal file
70
app/components/calendar/dialog/QuickCreateEvent.vue
Normal 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>
|
||||||
@@ -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,
|
||||||
@@ -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>
|
||||||
@@ -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') }}
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 }}
|
||||||
@@ -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";
|
||||||
|
|
||||||
46
app/components/calendar/menu/Menu.vue
Normal file
46
app/components/calendar/menu/Menu.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
41
app/components/calendar/menu/MenuToday.vue
Normal file
41
app/components/calendar/menu/MenuToday.vue
Normal 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>
|
||||||
@@ -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 }}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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" />
|
||||||
|
|
||||||
264
app/components/calendar/state/monthly/DayTile.vue
Normal file
264
app/components/calendar/state/monthly/DayTile.vue
Normal 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>
|
||||||
100
app/components/calendar/state/monthly/Layout.vue
Normal file
100
app/components/calendar/state/monthly/Layout.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
66
app/components/global/Breadcrumb.vue
Normal file
66
app/components/global/Breadcrumb.vue
Normal 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>
|
||||||
39
app/components/global/Navbar.vue
Normal file
39
app/components/global/Navbar.vue
Normal 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>
|
||||||
@@ -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"
|
||||||
22
app/components/global/card/Error.vue
Normal file
22
app/components/global/card/Error.vue
Normal 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>
|
||||||
@@ -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}`,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -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
|
||||||
343
app/components/global/input/Editor.vue
Normal file
343
app/components/global/input/Editor.vue
Normal 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>
|
||||||
18
app/components/global/sidebar/Footer.vue
Normal file
18
app/components/global/sidebar/Footer.vue
Normal 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>
|
||||||
|
|
||||||
50
app/components/global/sidebar/Menu.vue
Normal file
50
app/components/global/sidebar/Menu.vue
Normal 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>
|
||||||
32
app/components/global/sidebar/Sidebar.vue
Normal file
32
app/components/global/sidebar/Sidebar.vue
Normal 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>
|
||||||
16
app/components/global/sidebar/Toggle.vue
Normal file
16
app/components/global/sidebar/Toggle.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
43
app/components/global/user/DashboardLink.vue
Normal file
43
app/components/global/user/DashboardLink.vue
Normal 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>
|
||||||
@@ -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"] }>()
|
||||||
|
|
||||||
@@ -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"] }>()
|
||||||
|
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
9
app/components/ui/alert-dialog/index.ts
Normal file
9
app/components/ui/alert-dialog/index.ts
Normal 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"
|
||||||
@@ -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",
|
||||||
@@ -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: {
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
7
app/components/ui/breadcrumb/index.ts
Normal file
7
app/components/ui/breadcrumb/index.ts
Normal 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"
|
||||||
@@ -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>
|
||||||
@@ -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
Reference in New Issue
Block a user