Compare commits
265 Commits
features/n
...
upgrade/ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
015d0c4db4 | ||
|
|
392d8cd8ec | ||
|
|
473c69c9a1 | ||
|
|
790deeec5d | ||
|
|
687b96cf12 | ||
|
|
3e973dd0e9 | ||
|
|
cc07f898ea | ||
|
|
e5bc5c8a41 | ||
|
|
2faefa32ef | ||
|
|
d681c45a1e | ||
|
|
db6ae36743 | ||
|
|
6abf89e8db | ||
|
|
f1abae1cce | ||
|
|
a50824bd4f | ||
|
|
ed1252add4 | ||
|
|
1e0d840b41 | ||
|
|
58f3783b13 | ||
|
|
c25c10f7b7 | ||
|
|
f00e049f76 | ||
|
|
02fba36421 | ||
|
|
aa76b17c37 | ||
|
|
b32fab81f6 | ||
|
|
8005a8e9a2 | ||
|
|
d4d74db5d9 | ||
|
|
637a5cd7e3 | ||
|
|
2856185a3c | ||
|
|
d3a1ff92e2 | ||
|
|
d2295024bd | ||
|
|
149d0794e3 | ||
|
|
7f3a2b8d56 | ||
|
|
6992bca957 | ||
|
|
f3fd97e3cd | ||
|
|
e6b82e9b85 | ||
|
|
15458ab5fd | ||
|
|
6add25f45f | ||
|
|
1bee00ffd6 | ||
|
|
404174de31 | ||
|
|
5b38b3d0fa | ||
|
|
586adee6a9 | ||
|
|
f94c688baa | ||
|
|
9034406a45 | ||
|
|
9e814e319f | ||
|
|
a1dc761904 | ||
|
|
c3c4554e09 | ||
|
|
c249dc8334 | ||
|
|
895d8a887b | ||
|
|
0126d42762 | ||
|
|
1de5f13df6 | ||
|
|
f0e3fc2167 | ||
|
|
fdb77a20f3 | ||
|
|
2cdf793e67 | ||
|
|
d7b299e0eb | ||
|
|
04ecbe1c2b | ||
|
|
267ed70f5a | ||
|
|
2716e6d559 | ||
|
|
5664204fce | ||
|
|
e0d5f679b0 | ||
|
|
ae8937e7a6 | ||
|
|
655f5d20f3 | ||
|
|
965452671b | ||
|
|
31a4316186 | ||
|
|
d46da12745 | ||
|
|
1aa3350da3 | ||
|
|
8848fa75b1 | ||
|
|
282aabb7fa | ||
|
|
204de011f6 | ||
|
|
d73fe93812 | ||
|
|
5df57ee587 | ||
|
|
3f4cb46ebf | ||
|
|
922bcdc64b | ||
|
|
a83bc240b1 | ||
|
|
7e2989e383 | ||
|
|
71866b0e4b | ||
|
|
ba3eb29e04 | ||
|
|
189f0eec75 | ||
|
|
4dbd80fa80 | ||
|
|
b2ec583a09 | ||
|
|
d663253a05 | ||
|
|
0cc6d0f1ee | ||
|
|
9cc0eabe3c | ||
|
|
2e63836b80 | ||
|
|
fe035da7c3 | ||
|
|
04940aacc3 | ||
|
|
d04cee2d12 | ||
|
|
879ff732ef | ||
|
|
500941ae69 | ||
|
|
d6d173d087 | ||
|
|
c59d4eaf92 | ||
|
|
60755fd46f | ||
|
|
8eecad6488 | ||
|
|
8ff5d5941a | ||
|
|
fe9a58b0ea | ||
|
|
dcb637021e | ||
|
|
5689d2689d | ||
|
|
91048d1b8e | ||
|
|
3db1654682 | ||
|
|
9f6d318a15 | ||
|
|
eedbf1decb | ||
|
|
bc6cbd839f | ||
|
|
bf8257e60f | ||
|
|
199cfb6634 | ||
|
|
df8af541c1 | ||
|
|
dde3f8504a | ||
|
|
01fe9f3281 | ||
|
|
bc7a400826 | ||
|
|
c99059d9a8 | ||
|
|
cd965f61e8 | ||
|
|
9d5ff6a0b8 | ||
|
|
06fccd83ca | ||
|
|
5f8ae1fb6c | ||
|
|
535a73162c | ||
|
|
49574d8ef1 | ||
|
|
323eedec56 | ||
|
|
4ddbfd9855 | ||
|
|
bb6df1d440 | ||
|
|
dd1cb1f420 | ||
|
|
11252c168e | ||
|
|
acd0476970 | ||
|
|
24c048df4d | ||
|
|
c55d690f8b | ||
|
|
39e4ac33b7 | ||
|
|
568efa2251 | ||
|
|
6a68105a0f | ||
|
|
bba01fdcf1 | ||
|
|
76a6468a04 | ||
|
|
9fa6af2fa3 | ||
|
|
1f15a66505 | ||
|
|
8562ea6362 | ||
|
|
378f07a862 | ||
|
|
d774fde1dd | ||
|
|
863937bb16 | ||
|
|
372c7a49e1 | ||
|
|
c7069164b0 | ||
|
|
5258ed3f2a | ||
|
|
66e6f84f8b | ||
|
|
190a94f96e | ||
|
|
bbf1cbf84e | ||
|
|
c58176a9e2 | ||
|
|
e5fb40c24c | ||
|
|
bf36be3aa1 | ||
|
|
636623ee96 | ||
|
|
c19262e3c2 | ||
|
|
f6898da2f2 | ||
|
|
0f5bc219f2 | ||
|
|
2f79de8483 | ||
|
|
a69553e089 | ||
|
|
680e5fe35c | ||
|
|
36bffbd0ab | ||
|
|
63c1cef77e | ||
|
|
11d900e7f7 | ||
|
|
f42a68dfc9 | ||
|
|
74427a5056 | ||
|
|
872e1be949 | ||
|
|
49cb74df7d | ||
|
|
6cfcde3b58 | ||
|
|
6fee4c7158 | ||
|
|
85c661eda5 | ||
|
|
6d66b599cf | ||
|
|
edb939bb39 | ||
|
|
fc91e4825a | ||
|
|
7f2b5e2811 | ||
|
|
9863ac9778 | ||
|
|
c1b60942d2 | ||
|
|
44b0339874 | ||
|
|
6782b9bd41 | ||
|
|
e71a810acd | ||
|
|
67aefb8bd9 | ||
|
|
386b714f3c | ||
|
|
cbc5bbeee0 | ||
|
|
168a0b61d5 | ||
|
|
4bc1e77e76 | ||
|
|
ebc715a3a6 | ||
|
|
3f201b90e3 | ||
|
|
8b1eb9c258 | ||
|
|
23a9f68b7b | ||
|
|
05ae4bfb1f | ||
|
|
6c73578b7f | ||
|
|
5a47cb3563 | ||
|
|
07e97873ef | ||
|
|
feb04912ca | ||
|
|
1985ffdbe9 | ||
|
|
245b46661a | ||
|
|
99e478951d | ||
|
|
3c8a018066 | ||
|
|
69b294dcf6 | ||
|
|
e3a6ff8790 | ||
|
|
19a71d356e | ||
|
|
23e996ee2e | ||
|
|
7c81d633e5 | ||
|
|
a7cf648f2f | ||
|
|
db96e66b7a | ||
|
|
73ff951a10 | ||
|
|
9f4f30cebe | ||
|
|
1137b172e4 | ||
|
|
9062c4f946 | ||
|
|
d54e7eb9e9 | ||
|
|
fe98b770e1 | ||
|
|
c84099fa54 | ||
|
|
065b83debd | ||
|
|
06dd1d976d | ||
|
|
e978acfc71 | ||
|
|
7244120219 | ||
|
|
0d57c70719 | ||
|
|
12bf5139da | ||
|
|
008ec4fcef | ||
|
|
328fc555ea | ||
|
|
bc29129546 | ||
|
|
a7cb032b12 | ||
|
|
fe8ab76aaf | ||
|
|
23365d6324 | ||
|
|
72fa9a982b | ||
|
|
9d44b900b0 | ||
|
|
8c6a60aa40 | ||
|
|
90be6baf42 | ||
|
|
87a73e7f43 | ||
|
|
9127346131 | ||
|
|
69f18e9338 | ||
|
|
a1aa512018 | ||
|
|
17b9937c6a | ||
|
|
e9ec2cb3ae | ||
|
|
15527fd983 | ||
|
|
92850861cd | ||
|
|
737ee6739b | ||
|
|
43768cb49d | ||
|
|
2330db31dc | ||
|
|
f7c135f498 | ||
|
|
f4ad06d744 | ||
|
|
8d19f0aca2 | ||
|
|
017b54dec7 | ||
|
|
78c1387602 | ||
|
|
8913a7d1b3 | ||
|
|
746897cb6e | ||
|
|
d7ecb379fd | ||
|
|
d88ede5e21 | ||
|
|
6bb874f799 | ||
|
|
6738ac76c4 | ||
|
|
721e26d8e4 | ||
|
|
a07afc8351 | ||
|
|
a58c1eeab9 | ||
|
|
0c48350271 | ||
|
|
c65bcc9ae9 | ||
|
|
7959ad2345 | ||
|
|
4c0a4031a1 | ||
|
|
9aa2d26135 | ||
|
|
9314a8c3c2 | ||
|
|
3002ae593c | ||
|
|
f963116018 | ||
|
|
4a4433dcdd | ||
|
|
ad79ca4186 | ||
|
|
6137ba3902 | ||
|
|
e63422f091 | ||
|
|
c6009a4bca | ||
|
|
94fcd1c04e | ||
|
|
0baf33ee19 | ||
|
|
85e6710560 | ||
|
|
4972c26385 | ||
|
|
05f5c5a92c | ||
|
|
edeaaa1e19 | ||
|
|
06b7dadf81 | ||
|
|
53024c6117 | ||
|
|
6749fdb46e | ||
|
|
28cdffa90b | ||
|
|
476ca5f949 | ||
|
|
170416a08f | ||
|
|
b71f7745a4 |
35
app.vue
35
app.vue
@@ -1,28 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ConfigProvider } from 'radix-vue'
|
||||
import { ConfigProvider } from "radix-vue"
|
||||
|
||||
useHead({
|
||||
titleTemplate: (titleChunk) => {
|
||||
return titleChunk ? `${titleChunk} — TTTools` : 'TTTools';
|
||||
return titleChunk ? `${titleChunk} — TTTools` : "TTTools";
|
||||
},
|
||||
meta: [
|
||||
{ name: "charset", content: "UTF-8" },
|
||||
{ name: "viewport", content: "width=device-width, initial-scale=1.0" },
|
||||
{ name: "author", content: "Alexis Pelé" },
|
||||
{ name: "description", content: "Tools destined to players and game master, helping them visualize their worlds better." },
|
||||
{ name: "generator", content: "Nuxt" },
|
||||
{ name: "msapplication-TileColor", content: "00aba9" },
|
||||
{ name: "theme-color", content: "00aba9" },
|
||||
{ name: "og:type", content: "tabletop-tools" },
|
||||
{ name: "og:url", content: "ttt.alexcreates.fr" },
|
||||
{ name: 'robots', content: 'noindex, nofollow'}
|
||||
{ name: "robots", content: "noindex, nofollow"}
|
||||
],
|
||||
link: [
|
||||
{ rel: 'apple-touch-icon', sizes: "76x76", href: '/apple-touch-icon.png' },
|
||||
{ rel: 'icon', sizes: "32x32", type: 'image/png', href: '/favicon-32x32.png' },
|
||||
{ rel: 'icon', sizes: "16x16", type: 'image/png', href: '/favicon-16x16.png' },
|
||||
{ rel: 'manifest', href: '/site.webmanifest' },
|
||||
{ rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#6595b4' },
|
||||
{ rel: "apple-touch-icon", sizes: "76x76", href: "/apple-touch-icon.png" },
|
||||
{ rel: "icon", sizes: "32x32", type: "image/png", href: "/favicon-32x32.png" },
|
||||
{ rel: "icon", sizes: "16x16", type: "image/png", href: "/favicon-16x16.png" },
|
||||
{ rel: "manifest", href: "/site.webmanifest" },
|
||||
{ rel: "mask-icon", href: "/safari-pinned-tab.svg", color: "#6595b4" },
|
||||
]
|
||||
})
|
||||
|
||||
@@ -35,8 +34,24 @@ const useIdFunction = () => useId()
|
||||
|
||||
<NuxtLayout>
|
||||
<ConfigProvider :use-id="useIdFunction">
|
||||
<NuxtPage/>
|
||||
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors">
|
||||
<Sidebar />
|
||||
|
||||
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</NuxtLayout>
|
||||
|
||||
<ClientOnly>
|
||||
<UiToaster />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrapper > * {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
301
assets/_colors.css
Normal file
301
assets/_colors.css
Normal file
@@ -0,0 +1,301 @@
|
||||
:root {
|
||||
--event-red-foreground: var(--color-slate-900);
|
||||
--event-red-background: var(--color-red-500);
|
||||
|
||||
--event-orange-foreground: var(--color-slate-900);
|
||||
--event-orange-background: var(--color-orange-500);
|
||||
|
||||
--event-amber-foreground: var(--color-slate-900);
|
||||
--event-amber-background: var(--color-amber-500);
|
||||
|
||||
--event-yellow-foreground: var(--color-slate-900);
|
||||
--event-yellow-background: var(--color-yellow-500);
|
||||
|
||||
--event-lime-foreground: var(--color-slate-900);
|
||||
--event-lime-background: var(--color-lime-500);
|
||||
|
||||
--event-green-foreground: var(--color-white);
|
||||
--event-green-background: var(--color-green-600);
|
||||
|
||||
--event-emerald-foreground: var(--color-white);
|
||||
--event-emerald-background: var(--color-emerald-600);
|
||||
|
||||
--event-teal-foreground: var(--color-white);
|
||||
--event-teal-background: var(--color-teal-600);
|
||||
|
||||
--event-cyan-foreground: var(--color-white);
|
||||
--event-cyan-background: var(--color-cyan-600);
|
||||
|
||||
--event-sky-foreground: var(--color-white);
|
||||
--event-sky-background: var(--color-sky-600);
|
||||
|
||||
--event-blue-foreground: var(--color-white);
|
||||
--event-blue-background: var(--color-blue-600);
|
||||
|
||||
--event-indigo-foreground: var(--color-white);
|
||||
--event-indigo-background: var(--color-indigo-600);
|
||||
|
||||
--event-violet-foreground: var(--color-white);
|
||||
--event-violet-background: var(--color-violet-600);
|
||||
|
||||
--event-purple-foreground: var(--color-white);
|
||||
--event-purple-background: var(--color-purple-600);
|
||||
|
||||
--event-fuchsia-foreground: var(--color-white);
|
||||
--event-fuchsia-background: var(--color-fuchsia-700);
|
||||
|
||||
--event-pink-foreground: var(--color-white);
|
||||
--event-pink-background: var(--color-pink-700);
|
||||
|
||||
--event-rose-foreground: var(--color-white);
|
||||
--event-rose-background: var(--color-pink-700);
|
||||
|
||||
--event-black-foreground: var(--color-white);
|
||||
--event-black-background: var(--color-stone-500);
|
||||
|
||||
--event-white-foreground: var(--color-slate-900);
|
||||
--event-white-background: var(--color-lime-100);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-event-red-foreground: var(--event-red-foreground);
|
||||
--color-event-red-background: var(--event-red-background);
|
||||
|
||||
--color-event-orange-foreground: var(--event-orange-foreground);
|
||||
--color-event-orange-background: var(--event-orange-background);
|
||||
|
||||
--color-event-amber-foreground: var(--event-amber-foreground);
|
||||
--color-event-amber-background: var(--event-amber-background);
|
||||
|
||||
--color-event-yellow-foreground: var(--event-yellow-foreground);
|
||||
--color-event-yellow-background: var(--event-yellow-background);
|
||||
|
||||
--color-event-lime-foreground: var(--event-lime-foreground);
|
||||
--color-event-lime-background: var(--event-lime-background);
|
||||
|
||||
--color-event-green-foreground: var(--event-green-foreground);
|
||||
--color-event-green-background: var(--event-green-background);
|
||||
|
||||
--color-event-emerald-foreground: var(--event-emerald-foreground);
|
||||
--color-event-emerald-background: var(--event-emerald-background);
|
||||
|
||||
--color-event-teal-foreground: var(--event-teal-foreground);
|
||||
--color-event-teal-background: var(--event-teal-background);
|
||||
|
||||
--color-event-cyan-foreground: var(--event-cyan-foreground);
|
||||
--color-event-cyan-background: var(--event-cyan-background);
|
||||
|
||||
--color-event-sky-foreground: var(--event-sky-foreground);
|
||||
--color-event-sky-background: var(--event-sky-background);
|
||||
|
||||
--color-event-blue-foreground: var(--event-blue-foreground);
|
||||
--color-event-blue-background: var(--event-blue-background);
|
||||
|
||||
--color-event-indigo-foreground: var(--event-indigo-foreground);
|
||||
--color-event-indigo-background: var(--event-indigo-background);
|
||||
|
||||
--color-event-violet-foreground: var(--event-violet-foreground);
|
||||
--color-event-violet-background: var(--event-violet-background);
|
||||
|
||||
--color-event-purple-foreground: var(--event-purple-foreground);
|
||||
--color-event-purple-background: var(--event-purple-background);
|
||||
|
||||
--color-event-fuchsia-foreground: var(--event-fuchsia-foreground);
|
||||
--color-event-fuchsia-background: var(--event-fuchsia-background);
|
||||
|
||||
--color-event-pink-foreground: var(--event-pink-foreground);
|
||||
--color-event-pink-background: var(--event-pink-background);
|
||||
|
||||
--color-event-rose-foreground: var(--event-rose-foreground);
|
||||
--color-event-rose-background: var(--event-rose-background);
|
||||
|
||||
--color-event-black-foreground: var(--event-black-foreground);
|
||||
--color-event-black-background: var(--event-black-background);
|
||||
|
||||
--color-event-white-foreground: var(--event-white-foreground);
|
||||
--color-event-white-background: var(--event-white-background);
|
||||
}
|
||||
|
||||
.event-button,
|
||||
.event-popover,
|
||||
.event-callout {
|
||||
--fg-color: var(--color-foreground);
|
||||
--bg-color: var(--color-background);
|
||||
|
||||
transition-duration: var(--default-transition-duration);
|
||||
transition-timing-function: var(--default-transition-timing-function);
|
||||
}
|
||||
|
||||
.event-button {
|
||||
color: var(--fg-color);
|
||||
background-color: var(--bg-color);
|
||||
text-underline-offset: .15rem;
|
||||
transition-property: background-color, color;
|
||||
}
|
||||
.event-button:hover {
|
||||
text-decoration: underline;
|
||||
background-color: color-mix(in srgb, var(--bg-color) 70%, var(--color-background));
|
||||
}
|
||||
.event-button.is-hidden {
|
||||
background-color: transparent;
|
||||
color: var(--bg-color);
|
||||
border: 1px dashed var(--bg-color);
|
||||
transition-property: background-color, color, border-color;
|
||||
}
|
||||
.event-button.is-hidden:is(:hover, :focus-visible) {
|
||||
background-color: color-mix(in srgb, var(--bg-color) 70%, var(--color-background));
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.event-popover {
|
||||
background-color: color-mix(in srgb, var(--bg-color) 10%, var(--color-background));
|
||||
border-color: color-mix(in srgb, var(--bg-color) 50%, var(--color-background));
|
||||
transition-property: background-color, color;
|
||||
|
||||
hr {
|
||||
border-color: var(--bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.event-callout {
|
||||
position: relative;
|
||||
background-color: color-mix(in srgb, var(--bg-color) 10%, var(--color-background));
|
||||
border-top-right-radius: .2rem;
|
||||
border-bottom-right-radius: .2rem;
|
||||
transition-property: background-color, color;
|
||||
|
||||
hr {
|
||||
border-color: var(--bg-color);
|
||||
}
|
||||
}
|
||||
.event-callout::before {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
inset-block: 0;
|
||||
left: -0.5rem;
|
||||
width: .5rem;
|
||||
background-color: color-mix(in srgb, var(--bg-color) 70%, var(--color-background));
|
||||
border-top-left-radius: .2rem;
|
||||
border-bottom-left-radius: .2rem;
|
||||
transition-property: background-color;
|
||||
transition-duration: var(--default-transition-duration);
|
||||
transition-timing-function: var(--default-transition-timing-function);
|
||||
}
|
||||
.event-callout:is(:hover, :focus-visible) {
|
||||
background-color: color-mix(in srgb, var(--bg-color) 30%, var(--color-background));
|
||||
}
|
||||
.event-callout:is(:hover, :focus-visible)::before {
|
||||
background-color: color-mix(in srgb, var(--bg-color) 90%, var(--color-background));
|
||||
}
|
||||
|
||||
.event-callout.is-hidden {
|
||||
border: 1px dashed var(--bg-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
.event-callout.is-hidden::before {
|
||||
inset-block: -2px;
|
||||
height: calc(100% + 3px);
|
||||
background-color: color-mix(in srgb, var(--bg-color) 40%, var(--color-background));
|
||||
}
|
||||
.event-callout.is-hidden:is(:hover, :focus-visible) {
|
||||
background-color: color-mix(in srgb, var(--bg-color) 5%, var(--color-background));
|
||||
}
|
||||
.event-callout.is-hidden:is(:hover, :focus-visible)::before {
|
||||
background-color: color-mix(in srgb, var(--bg-color) 50%, var(--color-background));
|
||||
}
|
||||
|
||||
.bgc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.bgc::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: .75rem;
|
||||
aspect-ratio: 1;
|
||||
margin-right: 0.5em;
|
||||
border-radius: .25rem;
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.element-red {
|
||||
--fg-color: var(--color-event-red-foreground);
|
||||
--bg-color: var(--color-event-red-background);
|
||||
}
|
||||
.element-orange {
|
||||
--fg-color: var(--color-event-orange-foreground);
|
||||
--bg-color: var(--color-event-orange-background);
|
||||
}
|
||||
.element-amber {
|
||||
--fg-color: var(--color-event-amber-foreground);
|
||||
--bg-color: var(--color-event-amber-background);
|
||||
}
|
||||
.element-yellow {
|
||||
--fg-color: var(--color-event-yellow-foreground);
|
||||
--bg-color: var(--color-event-yellow-background);
|
||||
}
|
||||
.element-lime {
|
||||
--fg-color: var(--color-event-lime-foreground);
|
||||
--bg-color: var(--color-event-lime-background);
|
||||
}
|
||||
.element-green {
|
||||
--fg-color: var(--color-event-green-foreground);
|
||||
--bg-color: var(--color-event-green-background);
|
||||
}
|
||||
.element-emerald {
|
||||
--fg-color: var(--color-event-emerald-foreground);
|
||||
--bg-color: var(--color-event-emerald-background);
|
||||
}
|
||||
.element-teal {
|
||||
--fg-color: var(--color-event-teal-foreground);
|
||||
--bg-color: var(--color-event-teal-background);
|
||||
}
|
||||
.element-cyan {
|
||||
--fg-color: var(--color-event-cyan-foreground);
|
||||
--bg-color: var(--color-event-cyan-background);
|
||||
}
|
||||
.element-sky {
|
||||
--fg-color: var(--color-event-sky-foreground);
|
||||
--bg-color: var(--color-event-sky-background);
|
||||
}
|
||||
.element-blue {
|
||||
--fg-color: var(--color-event-blue-foreground);
|
||||
--bg-color: var(--color-event-blue-background);
|
||||
}
|
||||
.element-indigo {
|
||||
--fg-color: var(--color-event-indigo-foreground);
|
||||
--bg-color: var(--color-event-indigo-background);
|
||||
}
|
||||
.element-violet {
|
||||
--fg-color: var(--color-event-violet-foreground);
|
||||
--bg-color: var(--color-event-violet-background);
|
||||
}
|
||||
.element-purple {
|
||||
--fg-color: var(--color-event-purple-foreground);
|
||||
--bg-color: var(--color-event-purple-background);
|
||||
}
|
||||
.element-fuchsia {
|
||||
--fg-color: var(--color-event-fuchsia-foreground);
|
||||
--bg-color: var(--color-event-fuchsia-background);
|
||||
}
|
||||
.element-pink {
|
||||
--fg-color: var(--color-event-pink-foreground);
|
||||
--bg-color: var(--color-event-pink-background);
|
||||
}
|
||||
.element-rose {
|
||||
--fg-color: var(--color-event-rose-foreground);
|
||||
--bg-color: var(--color-event-rose-background);
|
||||
}
|
||||
.element-black {
|
||||
--fg-color: var(--color-event-black-foreground);
|
||||
--bg-color: var(--color-event-black-background);
|
||||
}
|
||||
.element-white {
|
||||
--fg-color: var(--color-event-white-foreground);
|
||||
--bg-color: var(--color-event-white-background);
|
||||
}
|
||||
115
assets/main.css
115
assets/main.css
@@ -1,76 +1,47 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
@config '../tailwind.config.js';
|
||||
|
||||
@import "./theme.css" layer(theme);
|
||||
@import "./_colors.css" layer(theme);
|
||||
@import "./_colors.css" layer(utilities);
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-border, currentcolor);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
body {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
.fade-delay-enter-active,
|
||||
.fade-delay-leave-active {
|
||||
transition: all .5s ease 1s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to,
|
||||
.fade-delay-enter-from,
|
||||
.fade-delay-leave-to {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
108
assets/theme.css
Normal file
108
assets/theme.css
Normal file
@@ -0,0 +1,108 @@
|
||||
:root {
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(222.2 84% 4.9%);
|
||||
|
||||
--muted: hsl(210 40% 96.1%);
|
||||
--muted-foreground: hsl(215.4 16.3% 46.9%);
|
||||
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(222.2 84% 4.9%);
|
||||
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(222.2 84% 4.9%);
|
||||
|
||||
--border: hsl(214.3 31.8% 91.4%);
|
||||
--input: hsl(214.3 31.8% 91.4%);
|
||||
|
||||
--primary: hsl(245 58% 51%);
|
||||
--primary-foreground: hsl(0 0% 100%);
|
||||
|
||||
--secondary: hsl(210 50% 95%);
|
||||
--secondary-foreground: hsl(222.2 47.4% 11.2%);
|
||||
|
||||
--accent: hsl(210 40% 96.1%);
|
||||
--accent-foreground: hsl(222.2 47.4% 11.2%);
|
||||
|
||||
--destructive: hsl(0 84.2% 60.2%);
|
||||
--destructive-foreground: hsl(210 40% 98%);
|
||||
|
||||
--success: hsl(156 72% 67%);
|
||||
--success-muted: hsl(156 72% 90%);
|
||||
--success-foreground: hsl(222.2 84% 4.9%);
|
||||
|
||||
--ring: hsl(222.2 84% 4.9%);
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(180 0 4.9%);
|
||||
--foreground: hsl(210 40% 98%);
|
||||
|
||||
--muted: hsl(244 47% 20%);
|
||||
--muted-foreground: hsl(215 20.2% 65.1%);
|
||||
|
||||
--popover: hsl(180 0 4.9%);
|
||||
--popover-foreground: hsl(210 40% 98%);
|
||||
|
||||
--card: hsl(180 0 4.9%);
|
||||
--card-foreground: hsl(210 40% 98%);
|
||||
|
||||
--border: hsl(0, 0%, 16%);
|
||||
--input: hsl(244 47% 20%);
|
||||
|
||||
--primary: hsl(245 58% 51%);
|
||||
--primary-foreground: hsl(0 0% 100%);
|
||||
|
||||
--secondary: hsl(244 47% 20%);
|
||||
--secondary-foreground: hsl(210 40% 98%);
|
||||
|
||||
--accent: hsl(244 47% 20%);
|
||||
--accent-foreground: hsl(210 40% 98%);
|
||||
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive-foreground: hsl(210 40% 98%);
|
||||
|
||||
--success: hsl(142 72% 29%);
|
||||
--success-muted: hsl(142 72% 10%);
|
||||
--success-foreground: hsl(210 40% 98%);
|
||||
|
||||
--ring: hsl(212.7 26.8% 83.9%);
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
|
||||
--color-success: var(--success);
|
||||
--color-success-muted: var(--success-muted);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tsConfigPath": ".nuxt/tsconfig.json",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "assets/main.css",
|
||||
|
||||
@@ -1,87 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { computed, type Component, type ComputedRef } from 'vue'
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
import { computed, type Component, type ComputedRef } from "vue"
|
||||
|
||||
import MonthlyLayout from './state/monthly/Layout.vue'
|
||||
import CenturyLayout from './state/centennially/Layout.vue'
|
||||
import DecadeLayout from './state/decennially/Layout.vue'
|
||||
import YearLayout from './state/yearly/Layout.vue'
|
||||
// import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import MonthlyLayout from "./state/monthly/Layout.vue"
|
||||
import CenturyLayout from "./state/centennially/Layout.vue"
|
||||
import DecadeLayout from "./state/decennially/Layout.vue"
|
||||
import YearLayout from "./state/yearly/Layout.vue"
|
||||
|
||||
const { isAdvancedSearchOpen, isEditEventModalOpen } = storeToRefs(useCalendar())
|
||||
|
||||
const route = useRoute()
|
||||
const worldId = route.params.id
|
||||
|
||||
const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
|
||||
const { setEvents } = useCalendarEvents()
|
||||
const { setCharacters } = useCharacters()
|
||||
|
||||
const { data: calendar, pending: calPending, refresh: calRefresh } = await useLazyFetch(`/api/calendars/query?world_id=${worldId}`)
|
||||
const { data: characters, pending: charPending, refresh: charRefresh } = await useLazyFetch(`/api/characters/query?world_id=${worldId}`)
|
||||
|
||||
if (!calendar.value) {
|
||||
await calRefresh()
|
||||
} else {
|
||||
if (calendar.value?.data?.id) {
|
||||
setCalendarId(calendar.value?.data?.id)
|
||||
}
|
||||
if (calendar.value?.data?.months) {
|
||||
setMonths(calendar.value?.data?.months)
|
||||
}
|
||||
if (calendar.value?.data?.months) {
|
||||
setMonths(calendar.value?.data?.months)
|
||||
}
|
||||
if (calendar.value?.data?.today) {
|
||||
setDefaultDate(calendar.value?.data?.today)
|
||||
}
|
||||
if (calendar.value?.data?.events) {
|
||||
setEvents(calendar.value?.data?.events)
|
||||
}
|
||||
}
|
||||
if (!characters.value) {
|
||||
await charRefresh()
|
||||
} else {
|
||||
if (characters.value?.data) {
|
||||
setCharacters(characters.value?.data)
|
||||
}
|
||||
}
|
||||
|
||||
watch(calPending, (newStatus) => {
|
||||
if (!newStatus) {
|
||||
if (calendar.value?.data?.id) {
|
||||
setCalendarId(calendar.value?.data?.id)
|
||||
}
|
||||
if (calendar.value?.data?.months) {
|
||||
setMonths(calendar.value?.data?.months)
|
||||
}
|
||||
if (calendar.value?.data?.today) {
|
||||
setDefaultDate(calendar.value?.data?.today)
|
||||
}
|
||||
if (calendar.value?.data?.events) {
|
||||
setEvents(calendar.value?.data?.events)
|
||||
}
|
||||
}
|
||||
})
|
||||
watch(charPending, (newStatus) => {
|
||||
if (!newStatus) {
|
||||
if (characters.value?.data) {
|
||||
setCharacters(characters.value?.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
|
||||
const { isReadOnly } = storeToRefs(useCalendar())
|
||||
|
||||
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
case "month":
|
||||
return MonthlyLayout
|
||||
|
||||
case 'year':
|
||||
case "year":
|
||||
return YearLayout
|
||||
|
||||
case 'decade':
|
||||
case "decade":
|
||||
return DecadeLayout
|
||||
|
||||
case 'century':
|
||||
case "century":
|
||||
default:
|
||||
return CenturyLayout
|
||||
}
|
||||
@@ -93,23 +34,35 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<template v-if="calPending || charPending">
|
||||
<div class="h-full grid place-items-center">
|
||||
Loading notamment
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-full grid grid-rows-[auto,1fr]">
|
||||
<CalendarMenu />
|
||||
<div class="h-full w-full relative">
|
||||
<div class="h-full grid grid-rows-[auto_1fr]">
|
||||
<CalendarMenu />
|
||||
|
||||
<KeepAlive>
|
||||
<component :is="currentViewComponent"/>
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</template>
|
||||
<component :is="currentViewComponent" />
|
||||
|
||||
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
|
||||
<CalendarFormUpdateEvent v-model:model-value="isEditEventModalOpen" />
|
||||
<LazyCalendarSearch />
|
||||
<LazyCalendarDialogCategories v-if="!isReadOnly" />
|
||||
<LazyCalendarDialogUpdateEvent v-if="!isReadOnly" />
|
||||
<LazyCalendarDialogDeleteEvent v-if="!isReadOnly" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.screen-move, /* apply transition to moving elements */
|
||||
.screen-enter-active,
|
||||
.screen-leave-active {
|
||||
transition: all .4s ease-out;
|
||||
}
|
||||
|
||||
.screen-enter-from,
|
||||
.screen-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.screen-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { PhMapPin } from '@phosphor-icons/vue'
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, getRelativeString } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
|
||||
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
|
||||
|
||||
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<h1 class="text-2xl font-bold flex items-center gap-1">
|
||||
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
||||
</h1>
|
||||
<h2 class="text-lg italic opacity-75">
|
||||
{{ dateDifference }}
|
||||
</h2>
|
||||
|
||||
<template #fallback>
|
||||
<div class="grid gap-1">
|
||||
<UiSkeleton class="h-8 w-64" />
|
||||
<UiSkeleton class="h-6 w-28" />
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
@@ -1,70 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { RPGDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '~/models/CalendarEvent'
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
tileDate: RPGDate
|
||||
}>()
|
||||
|
||||
const { areDatesIdentical } = useCalendar()
|
||||
|
||||
const spansMultipleDays = Boolean(props.event.startDate && props.event.endDate)
|
||||
|
||||
const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, props.event.startDate)
|
||||
const isEndEvent =
|
||||
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
|
||||
|
||||
const isPopoverDetailsOpen = ref<boolean>(false)
|
||||
|
||||
function handleClosePopover() {
|
||||
isPopoverDetailsOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverDetailsOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button
|
||||
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
:class="
|
||||
cn({
|
||||
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category?.name === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category?.name === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category?.name === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category?.name === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category?.name === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category?.name === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category?.name === 'joueurs',
|
||||
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category?.name === 'inauguration',
|
||||
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category?.name === 'invention',
|
||||
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category?.name === 'science',
|
||||
'text-slate-900 bg-white hover:bg-yellow-200': event.category?.name === 'bénédiction',
|
||||
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category?.name === 'découverte',
|
||||
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category?.name === 'exploration',
|
||||
'text-white bg-amber-600 hover:bg-amber-700': event.category?.name === 'construction',
|
||||
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category?.name === 'arcanologie',
|
||||
'text-white bg-rose-600 hover:bg-rose-700': event.category?.name === 'criminalité',
|
||||
'text-white bg-stone-600 hover:bg-stone-700': event.category?.name === 'scandale',
|
||||
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category?.name === 'commerce',
|
||||
'rounded-r-none': isStartEvent,
|
||||
'rounded-l-none': isEndEvent
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ event.title }}
|
||||
</button>
|
||||
</UiPopoverTrigger>
|
||||
|
||||
<CalendarEventDetails
|
||||
:event
|
||||
:spans-multiple-days
|
||||
:is-start-event
|
||||
:is-end-event
|
||||
@query:close-popover="handleClosePopover"
|
||||
/>
|
||||
</UiPopover>
|
||||
</template>
|
||||
@@ -1,247 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/CalendarEvent'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import {
|
||||
PhHourglassMedium,
|
||||
PhAlarm,
|
||||
PhHourglassHigh,
|
||||
PhHourglassLow,
|
||||
PhDotsThreeOutlineVertical
|
||||
} from '@phosphor-icons/vue'
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, jumpToDate, revealEditEventModal, getRelativeString } = useCalendar()
|
||||
const { deleteEvent } = useCalendarEvents()
|
||||
const { lastActiveEvent } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
spansMultipleDays: boolean
|
||||
isStartEvent?: boolean
|
||||
isEndEvent?: boolean
|
||||
}>()
|
||||
|
||||
// Ref for the popover
|
||||
const eventDetails = ref<HTMLElement>()
|
||||
|
||||
const emit = defineEmits(['query:close-popover'])
|
||||
|
||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||
const dateDuration: string | null = props.event.endDate
|
||||
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||
: null
|
||||
|
||||
function handleJumpToDate(date: RPGDate) {
|
||||
jumpToDate(date)
|
||||
emit('query:close-popover')
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit event
|
||||
*/
|
||||
const commandMenuOpened = ref(false)
|
||||
|
||||
function deployEditModal() {
|
||||
lastActiveEvent.value = { ...props.event }
|
||||
revealEditEventModal()
|
||||
commandMenuOpened.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopoverContent
|
||||
class="event-details w-96 bg-slate-900"
|
||||
:align="'center'"
|
||||
:align-offset="50"
|
||||
:side="'left'"
|
||||
:collision-padding="60"
|
||||
:hide-when-detached="true"
|
||||
:class="{
|
||||
'border-slate-800': !event.category,
|
||||
'border-lime-800': event.category?.name === 'naissance',
|
||||
'border-stone-600': event.category?.name === 'mort',
|
||||
'border-orange-800': event.category?.name === 'catastrophe',
|
||||
'border-pink-800': event.category?.name === 'catastrophe naturelle',
|
||||
'border-sky-800': event.category?.name === 'législation',
|
||||
'border-purple-800': event.category?.name === 'religion',
|
||||
'border-emerald-800': event.category?.name === 'joueurs',
|
||||
'border-amber-800': event.category?.name === 'inauguration',
|
||||
'border-green-800': event.category?.name === 'invention',
|
||||
'border-cyan-800': event.category?.name === 'science',
|
||||
'border-slate-600': event.category?.name === 'bénédiction',
|
||||
'border-purple-700': event.category?.name === 'découverte',
|
||||
'border-indigo-700': event.category?.name === 'exploration',
|
||||
'border-amber-700': event.category?.name === 'construction',
|
||||
'border-violet-700': event.category?.name === 'arcanologie',
|
||||
'border-rose-800': event.category?.name === 'criminalité',
|
||||
'border-stone-700': event.category?.name === 'scandale',
|
||||
'border-yellow-600': event.category?.name === 'commerce'
|
||||
}"
|
||||
>
|
||||
<div ref="eventDetails" class="grid gap-1">
|
||||
<header class="pr-12">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="font-semibold">
|
||||
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mb-1 space-y-1">
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||
</p>
|
||||
<template v-if="dateDuration">
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="event.category || event.secondaryCategories">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||
{{ event.category?.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat.id">
|
||||
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||
{{ cat.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="event.description">
|
||||
<hr class="border-slate-500 mt-2" >
|
||||
|
||||
<div class="mt-2 text-sm text-slate-300">
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<menu class="absolute top-4 right-4">
|
||||
<UiPopover v-model:open="commandMenuOpened">
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton size="icon" variant="ghost">
|
||||
<PhDotsThreeOutlineVertical size="20" weight="fill" />
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-fit p-0" align="start" side="right" :collision-padding="20" >
|
||||
<UiCommand>
|
||||
<UiCommandList>
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem value="edit-event" @select="deployEditModal"> Modifier </UiCommandItem>
|
||||
<UiCommandItem value="delete-event" @select="deleteEvent(event.id!)"> Supprimer </UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</menu>
|
||||
|
||||
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer au début"
|
||||
@click="handleJumpToDate(event.startDate!)"
|
||||
>
|
||||
<PhHourglassHigh size="16" weight="fill" /> Début
|
||||
</button>
|
||||
</UiBadge>
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer à la fin"
|
||||
@click="handleJumpToDate(event.endDate!)"
|
||||
>
|
||||
<PhHourglassLow size="16" weight="fill" /> Fin
|
||||
</button>
|
||||
</UiBadge>
|
||||
</nav>
|
||||
</UiPopoverContent>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.border-slate-800 {
|
||||
--base-color: var(--color-slate-800);
|
||||
}
|
||||
.border-lime-800 {
|
||||
--base-color: var(--color-lime-800);
|
||||
}
|
||||
.border-stone-600 {
|
||||
--base-color: var(--color-stone-600);
|
||||
}
|
||||
.border-orange-800 {
|
||||
--base-color: var(--color-orange-800);
|
||||
}
|
||||
.border-pink-800 {
|
||||
--base-color: var(--color-pink-800);
|
||||
}
|
||||
.border-sky-800 {
|
||||
--base-color: var(--color-sky-800);
|
||||
}
|
||||
.border-purple-800 {
|
||||
--base-color: var(--color-purple-800);
|
||||
}
|
||||
.border-emerald-800 {
|
||||
--base-color: var(--color-emerald-800);
|
||||
}
|
||||
.border-amber-800 {
|
||||
--base-color: var(--color-amber-800);
|
||||
}
|
||||
.border-green-800 {
|
||||
--base-color: var(--color-green-800);
|
||||
}
|
||||
.border-cyan-800 {
|
||||
--base-color: var(--color-cyan-800);
|
||||
}
|
||||
.border-slate-600 {
|
||||
--base-color: var(--color-slate-600);
|
||||
}
|
||||
.border-purple-700 {
|
||||
--base-color: var(--color-purple-700);
|
||||
}
|
||||
.border-indigo-700 {
|
||||
--base-color: var(--color-indigo-700);
|
||||
}
|
||||
.border-amber-700 {
|
||||
--base-color: var(--color-amber-700);
|
||||
}
|
||||
.border-violet-700 {
|
||||
--base-color: var(--color-violet-700);
|
||||
}
|
||||
.border-rose-800 {
|
||||
--base-color: var(--color-rose-800);
|
||||
}
|
||||
.border-stone-700 {
|
||||
--base-color: var(--color-stone-700);
|
||||
}
|
||||
.border-yellow-600 {
|
||||
--base-color: var(--color-yellow-600);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 85%);
|
||||
background-color: var(--bg-color);
|
||||
|
||||
hr {
|
||||
border-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,198 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import {
|
||||
PhCaretDoubleLeft,
|
||||
PhCaretDoubleRight,
|
||||
PhCaretLeft,
|
||||
PhCaretRight
|
||||
} from '@phosphor-icons/vue'
|
||||
|
||||
interface DirectionLabels {
|
||||
pastFar: string
|
||||
pastNear: string
|
||||
futureNear: string
|
||||
futureFar: string
|
||||
}
|
||||
|
||||
const { currentConfig, decrementMonth, incrementMonth, decrementYear, incrementYear } =
|
||||
useCalendar()
|
||||
|
||||
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
return {
|
||||
pastFar: 'Année précédente',
|
||||
pastNear: 'Mois précédent',
|
||||
futureNear: 'Mois suivant',
|
||||
futureFar: 'Année suivante'
|
||||
}
|
||||
|
||||
case 'year':
|
||||
return {
|
||||
pastFar: 'Décennie précédente',
|
||||
pastNear: 'Année précédente',
|
||||
futureNear: 'Année suivante',
|
||||
futureFar: 'Décennie suivante'
|
||||
}
|
||||
|
||||
case 'decade':
|
||||
return {
|
||||
pastFar: 'Siècle précédent',
|
||||
pastNear: 'Décennie précédente',
|
||||
futureNear: 'Décennie suivante',
|
||||
futureFar: 'Siècle suivant'
|
||||
}
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
return {
|
||||
pastFar: 'Millénaire précédent',
|
||||
pastNear: 'Siècle précédent',
|
||||
futureNear: 'Siècle suivant',
|
||||
futureFar: 'Millénaire suivant'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function toPastFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
decrementYear()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
decrementYear(10)
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
decrementYear(100)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
decrementYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toPastNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
decrementMonth()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
decrementYear()
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
decrementYear(10)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
decrementYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
incrementMonth()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
incrementYear()
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
incrementYear(10)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
incrementYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
incrementYear()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
incrementYear(10)
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
incrementYear(100)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
incrementYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toPastFar()">
|
||||
<PhCaretDoubleLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toPastNear()">
|
||||
<PhCaretLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toFutureNear()">
|
||||
<PhCaretRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toFutureFar()">
|
||||
<PhCaretDoubleRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,118 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { type RPGDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
|
||||
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue'
|
||||
|
||||
const { currentDate, currentConfig, jumpToDate } = useCalendar()
|
||||
const { getRelativeEventFromDate } = useCalendarEvents()
|
||||
|
||||
function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
|
||||
let fromDate: RPGDate
|
||||
|
||||
// To modify, obviously
|
||||
const daysPerMonth = 32
|
||||
const monthsPerYear = 10
|
||||
|
||||
const toDay = position === 'next' ? daysPerMonth : 1
|
||||
const toMonth = position === 'next' ? monthsPerYear : 0
|
||||
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'year':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: toMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
try {
|
||||
const { targetDate } = getRelativeEventFromDate(fromDate, position)
|
||||
|
||||
jumpToDate(targetDate)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div class="grid items-end w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm">
|
||||
<ClientOnly>
|
||||
<span>{{ currentDate.currentDateTitle }}</span>
|
||||
|
||||
<template #fallback>
|
||||
<span class="inline-block">
|
||||
<UiSkeleton class="h-[19px] w-full rounded-sm" />
|
||||
</span>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none"
|
||||
@click="handleGotoPreviousEventPage('prev')"
|
||||
>
|
||||
<PhArrowLineLeft size="22" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Précédente page à évènements</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none"
|
||||
@click="handleGotoPreviousEventPage('next')"
|
||||
>
|
||||
<PhArrowLineRight size="22" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Prochaine page à évènements</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { defaultDate, areDatesIdentical } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
|
||||
|
||||
const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
|
||||
|
||||
const isDefaultDate: ComputedRef<boolean> = computed<boolean>(() => areDatesIdentical(selectedDate.value, defaultDate))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<ClientOnly>
|
||||
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate">
|
||||
Aujourd'hui
|
||||
</UiButton>
|
||||
|
||||
<template #fallback>
|
||||
<UiButton size="sm">
|
||||
Aujourd'hui
|
||||
</UiButton>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ defaultDateFormatted }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</template>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { PhCalendarBlank } from '@phosphor-icons/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
||||
|
||||
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton size="sm" variant="secondary">
|
||||
<PhCalendarBlank size="18" weight="fill" />
|
||||
|
||||
{{ viewTypeTitle }}
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
|
||||
<UiDropdownMenuLabel>Mode d'affichage</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem
|
||||
v-for="option in viewTypeOptions"
|
||||
:key="option"
|
||||
@click="setViewType(option)"
|
||||
>
|
||||
{{ getViewTypeTitle(option) }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
23
components/calendar/CategoriesCTA.vue
Normal file
23
components/calendar/CategoriesCTA.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhTag } from "@phosphor-icons/vue"
|
||||
|
||||
const { toggleCategoriesModal } = useCalendar()
|
||||
const { isReadOnly } = storeToRefs(useCalendar())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
|
||||
<PhTag size="20" weight="light" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('entity.calendar.seeCategories') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</template>
|
||||
35
components/calendar/CurrentDate.vue
Normal file
35
components/calendar/CurrentDate.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia"
|
||||
import { computed } from "vue"
|
||||
|
||||
import { PhMapPin } from "@phosphor-icons/vue"
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
|
||||
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
|
||||
|
||||
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
||||
const isToday = computed(() => getDifferenceInDays(defaultDate, selectedDate.value) === 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h1 class="text-2xl font-bold flex items-center gap-1">
|
||||
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
||||
</h1>
|
||||
<h2 v-if="!isToday" class="text-xl italic opacity-75">
|
||||
– {{ dateDifference }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<template #fallback>
|
||||
<div class="flex items-center gap-1">
|
||||
<UiSkeleton class="h-8 w-64" />
|
||||
<UiSkeleton class="h-6 w-28" />
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
50
components/calendar/OptionsCTA.vue
Normal file
50
components/calendar/OptionsCTA.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
import { PhCalendarBlank, PhCheckCircle, PhGear } from "@phosphor-icons/vue"
|
||||
import { computed } from "vue"
|
||||
|
||||
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
||||
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="secondary" size="icon">
|
||||
<PhGear size="20" weight="fill" />
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
|
||||
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="40" class="text-right">
|
||||
<UiDropdownMenuArrow />
|
||||
<UiDropdownMenuLabel>
|
||||
{{ $t('entity.calendar.seeOptions') }}
|
||||
</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSub>
|
||||
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
|
||||
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">
|
||||
{{ $t('ui.displayMode') }}
|
||||
|
||||
<PhCalendarBlank size="18" weight="fill" />
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuSubTrigger>
|
||||
<UiDropdownMenuPortal>
|
||||
<UiDropdownMenuSubContent>
|
||||
<UiDropdownMenuItem
|
||||
v-for="option in viewTypeOptions"
|
||||
:key="option"
|
||||
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||
:class="cn({ 'text-emerald-600': viewTypeTitle === getViewTypeTitle(option) })"
|
||||
@click="setViewType(option)"
|
||||
>
|
||||
<PhCheckCircle v-if="viewTypeTitle === getViewTypeTitle(option)" size="20" weight="fill" />
|
||||
|
||||
{{ getViewTypeTitle(option) }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuSubContent>
|
||||
</UiDropdownMenuPortal>
|
||||
</UiDropdownMenuSub>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
102
components/calendar/PreviewCard.vue
Normal file
102
components/calendar/PreviewCard.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhArchive, PhCalendarDots, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
|
||||
import { DateTime } from "luxon";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
const props = defineProps<{
|
||||
calendar: Calendar,
|
||||
gmId?: string,
|
||||
showActions?: boolean,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["on-edit", "on-delete"])
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
const createdAt = DateTime.fromISO(props.calendar.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
|
||||
const updatedAt = computed<string>(() => props.calendar.updatedAt ? DateTime.fromISO(props.calendar.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
|
||||
|
||||
const user = useSupabaseUser();
|
||||
const isOwner = computed(() => user.value && props.gmId && user.value.id === props.gmId);
|
||||
const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calendar.id}` : `/calendars/${props.calendar.shortId}`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCard
|
||||
class="w-full h-full flex flex-col transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
|
||||
:link="calendarLink"
|
||||
:class="{
|
||||
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !calendar.color,
|
||||
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': calendar.color === 'red',
|
||||
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': calendar.color === 'orange',
|
||||
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': calendar.color === 'amber',
|
||||
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': calendar.color === 'yellow',
|
||||
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': calendar.color === 'lime',
|
||||
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': calendar.color === 'green',
|
||||
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': calendar.color === 'emerald',
|
||||
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': calendar.color === 'teal',
|
||||
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': calendar.color === 'cyan',
|
||||
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': calendar.color === 'sky',
|
||||
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': calendar.color === 'blue',
|
||||
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': calendar.color === 'indigo',
|
||||
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': calendar.color === 'violet',
|
||||
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': calendar.color === 'purple',
|
||||
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': calendar.color === 'fuchsia',
|
||||
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': calendar.color === 'pink',
|
||||
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': calendar.color === 'rose',
|
||||
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': calendar.color === 'black',
|
||||
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-200 dark:border-slate-700 dark:focus-within:outline-slate-100': calendar.color === 'white',
|
||||
}"
|
||||
>
|
||||
<UiCardHeader class="gap-4">
|
||||
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
|
||||
|
||||
<div v-if="calendar.state === 'published'" class="flex items-center gap-1 text-sm">
|
||||
<PhFile size="20" weight="fill" />
|
||||
<span>{{ $t('ui.contentState.published') }}</span>
|
||||
</div>
|
||||
<div v-if="calendar.state === 'draft'" class="flex items-center gap-1 text-sm">
|
||||
<PhFileDashed size="20" weight="fill" />
|
||||
<span>{{ $t('ui.contentState.draft') }}</span>
|
||||
</div>
|
||||
<div v-if="calendar.state === 'archived'" class="flex items-center gap-1 text-sm">
|
||||
<PhArchive size="20" weight="fill" />
|
||||
<span>{{ $t('ui.contentState.archived') }}</span>
|
||||
</div>
|
||||
</UiCardHeader>
|
||||
|
||||
<UiCardContent class="grow">
|
||||
<p class="flex items-center gap-1">
|
||||
<PhCalendarDots size="24" weight="fill" />
|
||||
<span v-if="calendar.eventNb?.[0].count! > 1">{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
|
||||
<span v-else>{{ $t("entity.calendar.hasXEvent", { count: calendar.eventNb?.[0].count }) }}</span>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="isOwner && showActions"
|
||||
class="flex gap-1 absolute top-4 right-4 z-20"
|
||||
>
|
||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
|
||||
<PhPencil size="16" />
|
||||
</UiButton>
|
||||
|
||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
|
||||
<PhTrash size="16" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
<UiCardFooter>
|
||||
<ul class="grid gap-1 text-sm">
|
||||
<li class="flex gap-1 items-center">
|
||||
<PhFilePlus size="18" />
|
||||
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
|
||||
</li>
|
||||
<li v-if="updatedAt" class="flex gap-1 items-center">
|
||||
<PhPencilSimpleLine size="18" />
|
||||
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</UiCardFooter>
|
||||
</UiCard>
|
||||
</template>
|
||||
@@ -1,3 +1,3 @@
|
||||
export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-'']/g)
|
||||
|
||||
export type SearchMode = 'characters' | 'events' | undefined
|
||||
export type SearchMode = "characters" | "events" | undefined
|
||||
|
||||
15
components/calendar/category/List.vue
Normal file
15
components/calendar/category/List.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
defineProps<{
|
||||
categories: Category[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li v-for="category in categories" :key="category.id">
|
||||
<CalendarCategoryListItem :category="category" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
15
components/calendar/category/ListItem.vue
Normal file
15
components/calendar/category/ListItem.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
72
components/calendar/category/Table.vue
Normal file
72
components/calendar/category/Table.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from "~/models/Category";
|
||||
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { categories } = defineProps<{
|
||||
categories: Category[]
|
||||
}>()
|
||||
|
||||
const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.localeCompare(b.name)))
|
||||
|
||||
const deleteDialogOpened = ref<boolean>(false)
|
||||
|
||||
function openDeleteDialog() {
|
||||
deleteDialogOpened.value = true
|
||||
}
|
||||
|
||||
function closeDeleteDialog() {
|
||||
deleteDialogOpened.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<CalendarCategoryTableHeader />
|
||||
|
||||
<ScrollAreaRoot class="h-36 grow overflow-hidden ">
|
||||
<ScrollAreaViewport class="w-full h-full pr-4" as-child>
|
||||
<div class="last:border-0">
|
||||
<CalendarCategoryTableRow
|
||||
v-for="item in sortedCategories"
|
||||
:key="item.id"
|
||||
:category="item"
|
||||
@on-delete-category="openDeleteDialog"
|
||||
/>
|
||||
</div>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar
|
||||
class="flex select-none touch-none p-0.5 bg-background transition-colors duration-100 ease-out hover:bg-foreground/5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5"
|
||||
orientation="vertical"
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
class="flex-1 bg-foreground/20 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full"
|
||||
/>
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
|
||||
<CalendarCategoryTableFooter />
|
||||
</div>
|
||||
|
||||
<UiDialog v-model:open="deleteDialogOpened">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="closeDeleteDialog"
|
||||
@focus-outside="closeDeleteDialog"
|
||||
@interact-outside="closeDeleteDialog"
|
||||
@pointer-down-outside="closeDeleteDialog"
|
||||
>
|
||||
<UiDialogTitle>
|
||||
{{ $t('entity.category.deleteDialog.title') }}
|
||||
</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
{{ $t('entity.category.deleteDialog.subtitle') }}
|
||||
</UiDialogDescription>
|
||||
|
||||
<CalendarFormDeleteCategory />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
130
components/calendar/category/TableFooter.vue
Normal file
130
components/calendar/category/TableFooter.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhPlus } from "@phosphor-icons/vue"
|
||||
import { useToast } from "~/components/ui/toast"
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
type FooterMode = "add" | "view"
|
||||
const currentMode = ref<FooterMode>("view")
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const { focused: inputFocused } = useFocus(inputRef)
|
||||
|
||||
/**
|
||||
* Toggle view mode options
|
||||
*/
|
||||
type ToggleViewOptions = {
|
||||
execution?: "now" | "nextTick"
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle view mode
|
||||
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||
*/
|
||||
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||
currentMode.value = "view"
|
||||
|
||||
if (options.execution === "now") {
|
||||
inputFocused.value = false
|
||||
} else {
|
||||
nextTick(() => {
|
||||
inputFocused.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle add mode
|
||||
*/
|
||||
function toggleAdd() {
|
||||
currentMode.value = "add"
|
||||
categorySkeleton.value = { name: "", color: "black" }
|
||||
|
||||
nextTick(() => {
|
||||
inputFocused.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||
|
||||
const { addCategoryFromSkeleton } = useCategoryStore()
|
||||
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||
|
||||
/**
|
||||
* Submit the update
|
||||
*/
|
||||
async function submitNew() {
|
||||
if (!categorySkeleton.value) return
|
||||
const newCategoryName = toRaw(categorySkeleton.value).name
|
||||
|
||||
const { error } = await tryCatch(addCategoryFromSkeleton())
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: t("entity.category.addedToast.titleError", { category: newCategoryName }),
|
||||
variant: "destructive",
|
||||
duration: ToastLifetime.LONG
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toggleView({ execution: "now" })
|
||||
toast({
|
||||
title: t("entity.category.addedToast.title", { category: newCategoryName }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-y-[1px] border-b-foreground/10 mr-4">
|
||||
<form
|
||||
ref="rowRef"
|
||||
class="grid grid-cols-12 items-center gap-4 p-1 bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900"
|
||||
@submit.prevent="submitNew"
|
||||
>
|
||||
<div v-if="currentMode === 'add'" class="col-span-1" />
|
||||
<div
|
||||
:class="cn({
|
||||
'col-span-6': currentMode === 'view',
|
||||
'col-span-5': currentMode === 'add'
|
||||
})"
|
||||
>
|
||||
<template v-if="currentMode === 'view'">
|
||||
<button
|
||||
class="p-2 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||
@click="toggleAdd"
|
||||
>
|
||||
<PhPlus size="18" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="categorySkeleton.name"
|
||||
type="text"
|
||||
class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||
<div class="-mx-2">
|
||||
<InputColor
|
||||
id="category-color"
|
||||
v-model="categorySkeleton.color"
|
||||
position="item-aligned"
|
||||
theme="subtle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
19
components/calendar/category/TableHeader.vue
Normal file
19
components/calendar/category/TableHeader.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { PhListBullets } from "@phosphor-icons/vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-border font-bold mr-4">
|
||||
<div class="col-span-1 flex items-end">
|
||||
<PhListBullets size="20" />
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
Nom
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
Couleur
|
||||
</div>
|
||||
<div class="col-span-2" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
187
components/calendar/category/TableRow.vue
Normal file
187
components/calendar/category/TableRow.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import { PhCheck, PhTrash } from "@phosphor-icons/vue"
|
||||
import { useToast } from "~/components/ui/toast"
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { Category } from "~/models/Category"
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { category } = defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "on-delete-category", payload: Category): void
|
||||
}>()
|
||||
|
||||
type RowMode = "edit" | "view"
|
||||
const currentMode = ref<RowMode>("view")
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const { focused: inputFocused } = useFocus(inputRef)
|
||||
const rowHovered = useElementHover(rowRef)
|
||||
|
||||
/**
|
||||
* Toggle view mode options
|
||||
*/
|
||||
type ToggleViewOptions = {
|
||||
execution?: "now" | "nextTick"
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle view mode
|
||||
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||
*/
|
||||
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||
currentMode.value = "view"
|
||||
|
||||
if (options.execution === "now") {
|
||||
inputFocused.value = false
|
||||
} else {
|
||||
nextTick(() => {
|
||||
inputFocused.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle edit mode
|
||||
*/
|
||||
function toggleEdit() {
|
||||
currentMode.value = "edit"
|
||||
categorySkeleton.value = structuredClone(toRaw(category))
|
||||
|
||||
nextTick(() => {
|
||||
inputFocused.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||
|
||||
const { resetSkeleton, updateCategoryFromSkeleton } = useCategoryStore()
|
||||
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||
|
||||
onUnmounted(() => {
|
||||
resetSkeleton()
|
||||
})
|
||||
|
||||
/**
|
||||
* Submit the update
|
||||
*/
|
||||
async function submitUpdate() {
|
||||
const { error } = await tryCatch(updateCategoryFromSkeleton())
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: t("entity.category.updatedToast.titleError", { category: category.name }),
|
||||
variant: "destructive",
|
||||
duration: ToastLifetime.LONG
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toggleView({ execution: "now" })
|
||||
toast({
|
||||
title: t("entity.category.updatedToast.title", { category: category.name }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
}
|
||||
|
||||
function handleQueryDelete() {
|
||||
categorySkeleton.value = structuredClone(toRaw(category))
|
||||
emit("on-delete-category", category)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rowRef" class="relative">
|
||||
<form
|
||||
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
|
||||
:class="cn(
|
||||
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
|
||||
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
|
||||
)"
|
||||
@submit.prevent="submitUpdate"
|
||||
>
|
||||
<div class="col-span-1 pointer-events-none">
|
||||
<span class="opacity-50 p-2">{{ category.id }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-5"
|
||||
>
|
||||
<template v-if="currentMode === 'view'">
|
||||
<button
|
||||
class="py-2 px-1 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||
@click="toggleEdit"
|
||||
>
|
||||
{{ category.name }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="categorySkeleton.name"
|
||||
type="text"
|
||||
class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<template v-if="currentMode === 'view'">
|
||||
<button
|
||||
class="p-1 h-full w-full text-left text-sm cursor-pointer"
|
||||
@click="toggleEdit"
|
||||
>
|
||||
<span
|
||||
class="bgc"
|
||||
:class="cn(`element-${category.color}`)"
|
||||
>
|
||||
{{ $t(`ui.colors.${category.color}`) }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||
<div class="-mx-2">
|
||||
<InputColor
|
||||
id="category-color"
|
||||
v-model="categorySkeleton.color"
|
||||
position="item-aligned"
|
||||
theme="subtle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
<menu class="w-fit absolute top-1/2 -translate-y-1/2 right-2 flex items-center gap-2">
|
||||
<li
|
||||
v-if="currentMode === 'edit' && categorySkeleton"
|
||||
>
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white"
|
||||
:title="$t('ui.actions.edit')"
|
||||
@click="submitUpdate"
|
||||
>
|
||||
<PhCheck size="14" weight="bold" />
|
||||
</UiButton>
|
||||
</li>
|
||||
<li v-else-if="rowHovered">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white"
|
||||
:title="$t('ui.actions.delete')"
|
||||
@click="handleQueryDelete"
|
||||
>
|
||||
<PhTrash size="14" weight="bold" />
|
||||
</UiButton>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
</template>
|
||||
41
components/calendar/dialog/Categories.vue
Normal file
41
components/calendar/dialog/Categories.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhX } from "@phosphor-icons/vue";
|
||||
|
||||
const { toggleCategoriesModal } = useCalendar()
|
||||
const { categories, isCategoriesModalOpen } = storeToRefs(useCalendar())
|
||||
|
||||
function handleClosing() {
|
||||
toggleCategoriesModal(false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog :open="isCategoriesModalOpen">
|
||||
<UiAlertDialogContent
|
||||
class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@close-auto-focus="(e) => e.preventDefault()"
|
||||
>
|
||||
<header>
|
||||
<UiAlertDialogTitle>
|
||||
<span class="text-2xl">
|
||||
{{ $t('entity.category.manageDialog.title') }}
|
||||
</span>
|
||||
</UiAlertDialogTitle>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
{{ $t('entity.category.manageDialog.subtitle') }}
|
||||
</UiAlertDialogDescription>
|
||||
</header>
|
||||
|
||||
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClosing">
|
||||
<PhX size="20" />
|
||||
</UiButton>
|
||||
|
||||
<CalendarCategoryTable :categories />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
47
components/calendar/dialog/Create.vue
Normal file
47
components/calendar/dialog/Create.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhX } from "@phosphor-icons/vue";
|
||||
import type { World } from "~/models/World";
|
||||
|
||||
defineProps<{
|
||||
world: World,
|
||||
modalState?: boolean
|
||||
}>()
|
||||
|
||||
const calendarSkeletonName = ref<string>("")
|
||||
|
||||
function onChangedName(newName: string) {
|
||||
calendarSkeletonName.value = newName
|
||||
}
|
||||
|
||||
const emit = defineEmits(["on-close"])
|
||||
|
||||
function handleClose() {
|
||||
emit("on-close")
|
||||
setTimeout(() => calendarSkeletonName.value = "", 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog :open="modalState">
|
||||
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
|
||||
<UiAlertDialogTitle>
|
||||
<span class="text-2xl">
|
||||
<strong class="font-bold">{{ world.name }}</strong>
|
||||
<span class="opacity-30"> — </span>
|
||||
<span v-if="calendarSkeletonName">
|
||||
{{ calendarSkeletonName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('entity.calendar.createDialog.title') }}
|
||||
</span>
|
||||
</span>
|
||||
</UiAlertDialogTitle>
|
||||
|
||||
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
|
||||
<PhX size="20" />
|
||||
</UiButton>
|
||||
|
||||
<CalendarFormCreate @on-changed-name="onChangedName" @on-close="handleClose" />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
62
components/calendar/dialog/CreateEvent.vue
Normal file
62
components/calendar/dialog/CreateEvent.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from "~/models/Date";
|
||||
|
||||
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
|
||||
const { resetSkeleton } = useCalendar()
|
||||
const popoverOpen = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
date?: RPGDate
|
||||
btnClass?: string
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Opens event creation's popover
|
||||
*/
|
||||
function openEventCreatePopover() {
|
||||
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
|
||||
if (operationInProgress.value) {
|
||||
popoverOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
resetSkeleton()
|
||||
|
||||
popoverOpen.value = true
|
||||
|
||||
// Set skeleton initial startDate if it's known
|
||||
if (props.date) {
|
||||
eventSkeleton.value.startDate = { ...props.date } // We need to clone it otherwise the props ends up mutating (?)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing() {
|
||||
popoverOpen.value = false
|
||||
resetSkeleton()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="popoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button :class="btnClass" @click="openEventCreatePopover()" />
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down.prevent="handleClosing"
|
||||
@pointer-down-outside.prevent="handleClosing"
|
||||
>
|
||||
<CalendarFormCreateEvent @event-created="handleClosing" />
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</template>
|
||||
112
components/calendar/dialog/Delete.vue
Normal file
112
components/calendar/dialog/Delete.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
modalState: boolean,
|
||||
calendar: Calendar | null
|
||||
}>()
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
const emit = defineEmits(["on-close"])
|
||||
|
||||
async function handleAction(): Promise<void> {
|
||||
if (isLoading.value) return
|
||||
if (!props.calendar) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await $fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" })
|
||||
emit("on-close")
|
||||
|
||||
toast({
|
||||
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (err instanceof Error) {
|
||||
toast({
|
||||
title: err.message,
|
||||
variant: "destructive"
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing() {
|
||||
if (!isLoading.value) {
|
||||
emit("on-close")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog :open="modalState">
|
||||
<UiAlertDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
@close-auto-focus="(e) => e.preventDefault()"
|
||||
>
|
||||
<UiAlertDialogTitle>
|
||||
{{ $t('entity.calendar.deleteDialog.title') }}
|
||||
</UiAlertDialogTitle>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
{{ $t('entity.calendar.deleteDialog.subtitle') }}
|
||||
</UiAlertDialogDescription>
|
||||
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
<!-- {{ formErrors.message }} -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-between">
|
||||
<UiButton type="button" size="sm" variant="outline" @click="handleClosing">
|
||||
{{ $t('ui.action.back') }}
|
||||
</UiButton>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton v-if="calendar" size="sm" variant="destructive" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('entity.deleteOne', { entity: calendar?.name }) }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
45
components/calendar/dialog/DeleteEvent.vue
Normal file
45
components/calendar/dialog/DeleteEvent.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
// Watch the popover state
|
||||
watch(isDeleteEventModalOpen, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = structuredClone(toRaw(lastActiveEvent.value))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event): void {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog v-model:open="isDeleteEventModalOpen">
|
||||
<UiAlertDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<UiAlertDialogTitle>{{ $t('entity.calendar.event.deleteDialog.title') }}</UiAlertDialogTitle>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
{{ $t('entity.calendar.event.deleteDialog.subtitle') }}
|
||||
</UiAlertDialogDescription>
|
||||
|
||||
<CalendarFormDeleteEvent />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
41
components/calendar/dialog/QuickCreateEvent.vue
Normal file
41
components/calendar/dialog/QuickCreateEvent.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { PhPlus } from "@phosphor-icons/vue";
|
||||
|
||||
const isDialogOpen = ref<boolean>(false);
|
||||
const { resetSkeleton } = useCalendar();
|
||||
|
||||
// Toggles the dialog
|
||||
function toggleDialog() {
|
||||
isDialogOpen.value = !isDialogOpen.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*/
|
||||
function handleClosing() {
|
||||
setTimeout(() => resetSkeleton(), 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiButton @click="toggleDialog">
|
||||
<PhPlus size="18" weight="bold" />
|
||||
|
||||
<strong class="font-semibold">
|
||||
{{ $t("entity.calendar.event.newEvent") }}
|
||||
</strong>
|
||||
</UiButton>
|
||||
|
||||
<UiDialog v-model:open="isDialogOpen">
|
||||
<UiDialogContent
|
||||
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down.prevent="handleClosing"
|
||||
@pointer-down-outside.prevent="handleClosing"
|
||||
>
|
||||
<UiDialogTitle>
|
||||
{{ $t("entity.calendar.event.addSingle") }}
|
||||
</UiDialogTitle>
|
||||
<CalendarFormCreateEvent @event-created="toggleDialog" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
49
components/calendar/dialog/Update.vue
Normal file
49
components/calendar/dialog/Update.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhX } from "@phosphor-icons/vue";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import type { World } from "~/models/World";
|
||||
|
||||
const props = defineProps<{
|
||||
calendar: Calendar | null,
|
||||
world: World | null,
|
||||
modalState?: boolean
|
||||
}>()
|
||||
|
||||
const calendarSkeletonName = ref<string>(props.calendar?.name ?? "")
|
||||
|
||||
function onChangedName(newName: string) {
|
||||
calendarSkeletonName.value = newName
|
||||
}
|
||||
|
||||
const emit = defineEmits(["on-close"])
|
||||
|
||||
function handleClose() {
|
||||
emit("on-close")
|
||||
setTimeout(() => calendarSkeletonName.value = "", 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog :open="modalState">
|
||||
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
|
||||
<UiAlertDialogTitle>
|
||||
<span class="text-2xl">
|
||||
<strong class="font-bold">{{ world?.name }}</strong>
|
||||
<span class="opacity-30"> — </span>
|
||||
<span v-if="calendarSkeletonName">
|
||||
{{ calendarSkeletonName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('entity.calendar.updateDialog.title') }}
|
||||
</span>
|
||||
</span>
|
||||
</UiAlertDialogTitle>
|
||||
|
||||
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
|
||||
<PhX size="20" />
|
||||
</UiButton>
|
||||
|
||||
<CalendarFormUpdate v-if="calendar" :world="world" :calendar="calendar" @on-changed-name="onChangedName" @on-close="handleClose" />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
47
components/calendar/dialog/UpdateEvent.vue
Normal file
47
components/calendar/dialog/UpdateEvent.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
|
||||
const { resetSkeleton } = useCalendar();
|
||||
|
||||
// Watch the popover state
|
||||
watch(isEditEventModalOpen, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = structuredClone(toRaw(lastActiveEvent.value))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing() {
|
||||
isEditEventModalOpen.value = false
|
||||
setTimeout(() => resetSkeleton(), 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="isEditEventModalOpen">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 min-w-96 border-border"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="(e) => e.preventDefault()"
|
||||
>
|
||||
<header class="pl-8 grid gap-y-2">
|
||||
<UiDialogTitle>
|
||||
{{ $t('entity.calendar.event.editDialog.title') }}
|
||||
</UiDialogTitle>
|
||||
|
||||
<UiDialogDescription>
|
||||
{{ $t('entity.calendar.event.editDialog.subtitle') }}
|
||||
</UiDialogDescription>
|
||||
</header>
|
||||
|
||||
<CalendarFormUpdateEvent @event-updated="handleClosing" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
207
components/calendar/event/Details.vue
Normal file
207
components/calendar/event/Details.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { RPGDate } from "@/models/Date"
|
||||
import type { CalendarEvent } from "@/models/CalendarEvent"
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
|
||||
import {
|
||||
PhHourglassMedium,
|
||||
PhAlarm,
|
||||
PhHourglassHigh,
|
||||
PhHourglassLow,
|
||||
PhMapPinArea,
|
||||
PhDotsThreeOutlineVertical,
|
||||
PhEye
|
||||
} from "@phosphor-icons/vue"
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
||||
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
spansMultipleDays: boolean
|
||||
isStartEvent?: boolean
|
||||
isEndEvent?: boolean
|
||||
}>()
|
||||
|
||||
// Ref for the popover
|
||||
const eventDetails = ref<HTMLElement>()
|
||||
|
||||
const emit = defineEmits(["query:close-popover"])
|
||||
|
||||
const dateDifference = computed<string>(() => getRelativeString(defaultDate, props.event.startDate))
|
||||
const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
|
||||
|
||||
function handleJumpToDate(date: RPGDate) {
|
||||
jumpToDate(date)
|
||||
emit("query:close-popover")
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit event
|
||||
*/
|
||||
const commandMenuOpened = ref(false)
|
||||
|
||||
function deployEditModal() {
|
||||
lastActiveEvent.value = structuredClone(toRaw(props.event))
|
||||
revealEditEventModal()
|
||||
commandMenuOpened.value = false
|
||||
emit("query:close-popover")
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm event deletion
|
||||
*/
|
||||
function deployDeleteModal() {
|
||||
lastActiveEvent.value = structuredClone(toRaw(props.event))
|
||||
revealDeleteEventModal()
|
||||
commandMenuOpened.value = false
|
||||
emit("query:close-popover")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopoverContent
|
||||
class="w-96 event-popover"
|
||||
:align="'center'"
|
||||
:align-offset="50"
|
||||
:side="'left'"
|
||||
:collision-padding="60"
|
||||
:hide-when-detached="true"
|
||||
:class="cn(
|
||||
event.category ? `element-${event.category.color}` : '',
|
||||
)"
|
||||
@focus-outside.prevent
|
||||
>
|
||||
<div ref="eventDetails" class="grid gap-1">
|
||||
<header class="pr-12">
|
||||
<div class="mb-2 max-h-24 overflow-y-auto text-lg font-semibold">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="font-semibold">
|
||||
{{
|
||||
$t('entity.calendar.date.fromTo',
|
||||
{
|
||||
startDate: getFormattedDateTitle(event.startDate, true),
|
||||
endDate: getFormattedDateTitle(event.endDate, true)
|
||||
}
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mb-1 space-y-1">
|
||||
<template v-if="event.location">
|
||||
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
|
||||
<div>
|
||||
<PhMapPinArea size="16" weight="fill" />
|
||||
</div>
|
||||
<p class="max-h-12 overflow-y-auto">{{ event.location }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
|
||||
<div>
|
||||
<PhAlarm size="16" weight="fill" />
|
||||
</div>
|
||||
<p>{{ dateDifference }}</p>
|
||||
</div>
|
||||
<template v-if="dateDuration">
|
||||
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
|
||||
<div>
|
||||
<PhHourglassMedium size="16" weight="fill" />
|
||||
</div>
|
||||
<p>{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="event.category || event.secondaryCategories">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<UiBadge class="mix-blend-luminosity font-bold bg-gray-400 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
|
||||
{{ event.category?.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat.id">
|
||||
<UiBadge class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
|
||||
{{ cat.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="event.description">
|
||||
<hr class="border-border mt-2" >
|
||||
|
||||
<div class="max-h-48 overflow-y-auto mt-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<menu v-if="!isReadOnly" class="absolute top-4 right-4" :class="cn({ 'top-6': event.hidden })">
|
||||
<UiPopover v-model:open="commandMenuOpened">
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton size="icon" variant="outline" class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600">
|
||||
<PhDotsThreeOutlineVertical size="20" weight="fill" />
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-fit p-0" align="start" side="right" :collision-padding="20" >
|
||||
<UiCommand>
|
||||
<UiCommandList>
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem value="edit-event" @select="deployEditModal"> {{ $t('ui.action.edit') }} </UiCommandItem>
|
||||
<UiCommandItem value="delete-event" @select="deployDeleteModal"> {{ $t('ui.action.delete') }} </UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</menu>
|
||||
|
||||
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
|
||||
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child>
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer au début"
|
||||
@click="handleJumpToDate(event.startDate!)"
|
||||
>
|
||||
<PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }}
|
||||
</button>
|
||||
</UiBadge>
|
||||
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child title="Naviguer à la fin">
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer à la fin"
|
||||
@click="handleJumpToDate(event.endDate!)"
|
||||
>
|
||||
<PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }}
|
||||
</button>
|
||||
</UiBadge>
|
||||
</nav>
|
||||
|
||||
<UiTooltipProvider v-if="event.hidden" :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiBadge class="absolute -top-2 right-2 flex gap-1">
|
||||
<PhEye size="16" weight="fill" /> {{ $t('entity.calendar.event.isHidden') }}
|
||||
</UiBadge>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('entity.calendar.event.hiddenTooltip') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</UiPopoverContent>
|
||||
</template>
|
||||
86
components/calendar/event/Event.vue
Normal file
86
components/calendar/event/Event.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { RPGDate } from "@/models/Date"
|
||||
import type { CalendarEvent } from "~/models/CalendarEvent"
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
tileDate: RPGDate
|
||||
}>()
|
||||
|
||||
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
||||
const { lastActiveEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
|
||||
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
|
||||
const isEndEvent = computed(() => spansMultipleDays.value && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate))
|
||||
|
||||
const titleCharLimit = 50;
|
||||
const eventTitle = computed<string>(() => props.event.title.length <= titleCharLimit ? props.event.title : `${props.event.title.slice(0, titleCharLimit)}…`)
|
||||
|
||||
// Popover code
|
||||
const isPopoverDetailsOpen = ref<boolean>(false)
|
||||
|
||||
function handleDoubleClick() {
|
||||
isPopoverDetailsOpen.value = false
|
||||
lastActiveEvent.value = structuredClone(toRaw(props.event))
|
||||
revealEditEventModal()
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
isPopoverDetailsOpen.value = false
|
||||
lastActiveEvent.value = structuredClone(toRaw(props.event))
|
||||
revealDeleteEventModal()
|
||||
}
|
||||
|
||||
function handleClosePopover() {
|
||||
isPopoverDetailsOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Listen for keydown events
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
// If the popover isn't opened, this is not the event we're trying to delete, so return
|
||||
if (!isPopoverDetailsOpen.value) return
|
||||
|
||||
// If the key isn't the delete one, return
|
||||
if (e.key !== "Delete") return
|
||||
|
||||
handleDelete()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverDetailsOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button
|
||||
class="event-button text-xs px-2 py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer"
|
||||
:class="
|
||||
cn(
|
||||
`element-${event.category?.color}`,
|
||||
{
|
||||
'is-hidden': event.hidden,
|
||||
'rounded-r-none': isStartEvent,
|
||||
'rounded-l-none': isEndEvent,
|
||||
}
|
||||
)
|
||||
"
|
||||
@dblclick="handleDoubleClick"
|
||||
@keydown.delete="handleDelete"
|
||||
>
|
||||
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
|
||||
{{ eventTitle }}
|
||||
</div>
|
||||
</button>
|
||||
</UiPopoverTrigger>
|
||||
|
||||
<LazyCalendarEventDetails
|
||||
:event
|
||||
:spans-multiple-days
|
||||
:is-start-event
|
||||
:is-end-event
|
||||
@query:close-popover="handleClosePopover"
|
||||
/>
|
||||
</UiPopover>
|
||||
</template>
|
||||
151
components/calendar/form/Create.vue
Normal file
151
components/calendar/form/Create.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
|
||||
|
||||
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft", color: "white" }
|
||||
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
|
||||
|
||||
onMounted(() => {
|
||||
calendarSkeleton.value = { ...defaultSkeleton }
|
||||
})
|
||||
|
||||
type FormTabs = "global" | "months" | "today"
|
||||
const activeTab = ref<FormTabs>("global")
|
||||
|
||||
/**
|
||||
* === Current date ===
|
||||
*/
|
||||
// If the months data change, just reset today's month
|
||||
// This is a failsafe mainly because of 1) month positions and 2) month names
|
||||
watch(calendarSkeleton.value.months, () => {
|
||||
calendarSkeleton.value.today.month = 0
|
||||
}, { deep: true })
|
||||
|
||||
/**
|
||||
* === Form Validation ===
|
||||
*/
|
||||
|
||||
/** Whether the skeleton has valid month data */
|
||||
const validSkeletonMonths = computed(() => calendarSkeleton.value.months.length > 0)
|
||||
|
||||
/** Whether the skeleton has a valid name */
|
||||
const validSkeletonGeneral = computed(() => calendarSkeleton.value.name)
|
||||
|
||||
/** Whether all the data checks above are a-ok */
|
||||
const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeletonMonths.value)
|
||||
|
||||
/** Send the data to the store for validation */
|
||||
const isCreatingCalendar = ref<boolean>(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
isCreatingCalendar.value = true
|
||||
await $fetch("/api/calendars/create", { method: "POST", body: { ...calendarSkeleton.value, worldId: 1 } })
|
||||
|
||||
emit("on-close")
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
isCreatingCalendar.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* === Watch for name changes to display above ===
|
||||
*/
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(e: "on-changed-name", calendarName: string): void
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(e: "on-close"): void
|
||||
}>()
|
||||
|
||||
/** Hook to emit a debounced event for the changed skeleton name */
|
||||
const handleNameChange = useDebounceFn(() => {
|
||||
emit("on-changed-name", calendarSkeleton.value.name)
|
||||
}, 400)
|
||||
|
||||
function handleFormCancel() {
|
||||
emit("on-close")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="calendarSkeleton">
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<UiTabs v-model:model-value="activeTab">
|
||||
<UiTabsList class="grid w-full grid-cols-3 mb-4">
|
||||
<UiTabsTrigger value="global" class="font-bold">
|
||||
<div class="flex items-center gap-1">
|
||||
<PhWrench size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.actionDialog.tabs.general.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
<UiTabsTrigger value="months" class="font-bold">
|
||||
<div class="flex items-center gap-1">
|
||||
<PhCalendarDots size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
<UiTabsTrigger value="today" class="font-bold">
|
||||
<div class="flex items-center gap-1">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
<UiTabsContent value="global" class="grid gap-4">
|
||||
<input
|
||||
id="new-calendar-name"
|
||||
v-model="calendarSkeleton.name"
|
||||
type="text"
|
||||
name="new-calendar-name"
|
||||
required
|
||||
:placeholder="$t('common.title')"
|
||||
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
|
||||
@input="handleNameChange"
|
||||
>
|
||||
|
||||
<div class="-mx-1 px-1 grid gap-3">
|
||||
<UiLabel for="new-calendar-state">
|
||||
{{ $t('ui.contentState.label') }}
|
||||
</UiLabel>
|
||||
|
||||
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
|
||||
</div>
|
||||
|
||||
<div class="-mx-1 grid gap-3">
|
||||
<UiLabel for="new-calendar-color">
|
||||
{{ $t('ui.colors.label') }}
|
||||
</UiLabel>
|
||||
|
||||
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
|
||||
</div>
|
||||
</UiTabsContent>
|
||||
<UiTabsContent value="months">
|
||||
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
|
||||
</UiTabsContent>
|
||||
<UiTabsContent value="today">
|
||||
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
|
||||
</UiTabsContent>
|
||||
</UiTabs>
|
||||
|
||||
<footer class="flex justify-end gap-2 mt-6">
|
||||
<UiButton type="button" variant="destructive" @click="handleFormCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
|
||||
<UiButton type="submit" :disabled="!validSkeleton || isCreatingCalendar">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isCreatingCalendar" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,126 +1,195 @@
|
||||
<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
|
||||
} from '@phosphor-icons/vue'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { eventSkeleton } = storeToRefs(useCalendarEvents())
|
||||
const { resetSkeleton, submitSkeleton } = useCalendarEvents()
|
||||
const popoverOpen = ref(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
const emit = defineEmits(["event-created"])
|
||||
|
||||
const props = defineProps<{
|
||||
date?: RPGDate
|
||||
btnClass?: string
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Opens event creation's popover
|
||||
*/
|
||||
function openEventCreatePopover() {
|
||||
resetSkeleton()
|
||||
const { eventSkeleton } = storeToRefs(useCalendar())
|
||||
const { submitSkeleton, cancelLatestRequest } = useCalendar()
|
||||
|
||||
popoverOpen.value = true
|
||||
const newEventTitle = shallowRef<HTMLInputElement>()
|
||||
useFocus(newEventTitle, { initialValue: true })
|
||||
|
||||
// Set skeleton initial startDate if it's known
|
||||
if (props.date) {
|
||||
eventSkeleton.value.startDate = { ...props.date } // We need to clone it otherwise the props ends up mutating (?)
|
||||
}
|
||||
}
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Watch the popover state
|
||||
// And reset the skeleton if it has been dismissed
|
||||
watch(popoverOpen, (hasOpened, _o) => {
|
||||
if (!hasOpened) {
|
||||
resetSkeleton()
|
||||
}
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
// Prevent form submission if already loading
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await submitSkeleton()
|
||||
|
||||
popoverOpen.value = false
|
||||
emit("event-created")
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel() {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="popoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button :class="btnClass" @click="openEventCreatePopover()" />
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="col-span-2 pl-8">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-2 gap-y-3">
|
||||
<div class="col-span-2 pl-8">
|
||||
<input
|
||||
id="new-event-title"
|
||||
ref="newEventTitle"
|
||||
v-model="eventSkeleton.title"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
:placeholder="$t('entity.calendar.event.title')"
|
||||
:maxlength="120"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
|
||||
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.title') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 my-2 pl-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
:placeholder="$t('entity.addDescription')"
|
||||
:maxlength="1200"
|
||||
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
:placeholder="$t('entity.calendar.date.start')"
|
||||
:initial-date="props.date"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
:placeholder="$t('entity.calendar.date.end')"
|
||||
:initial-date="props.date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<CalendarInputEventCategory
|
||||
v-model="eventSkeleton.category"
|
||||
:placeholder="$t('entity.category.addPrimary')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col-span-2 mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhMapPinArea size="18" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-title"
|
||||
v-model="eventSkeleton.title"
|
||||
id="new-event-location"
|
||||
v-model="eventSkeleton.location"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
placeholder="Titre de l'évènement"
|
||||
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">
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 pl-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
placeholder="Description brève de l'évènement"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
placeholder="Date de début"
|
||||
:initial-date="props.date"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
placeholder="Date de fin"
|
||||
:initial-date="props.date"
|
||||
/>
|
||||
name="new-event-location"
|
||||
:placeholder="$t('entity.calendar.event.addLocation')"
|
||||
:maxlength="160"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
|
||||
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 pl-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
|
||||
<PhEyeClosed v-else size="18" />
|
||||
|
||||
<div class="text-right">
|
||||
<UiButton size="sm">
|
||||
Sauvegarder
|
||||
</UiButton>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
|
||||
<UiLabel for="new-event-visibility">
|
||||
<template v-if="!eventSkeleton.hidden">
|
||||
{{ $t('entity.calendar.event.isPublic') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.event.isHidden') }}
|
||||
</template>
|
||||
</UiLabel>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 pl-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
85
components/calendar/form/DeleteCategory.vue
Normal file
85
components/calendar/form/DeleteCategory.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||
|
||||
const { deleteCategoryFromSkeleton, cancelLatestRequest } = useCategoryStore()
|
||||
const { isDeletingCategory, categorySkeleton } = storeToRefs(useCategoryStore())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
async function handleAction(): Promise<void> {
|
||||
if (isLoading.value && !categorySkeleton.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const categoryName = categorySkeleton.value?.name
|
||||
|
||||
try {
|
||||
await deleteCategoryFromSkeleton()
|
||||
|
||||
toast({
|
||||
title: t("entity.category.deletedToast.title", { category: categoryName }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.MEDIUM
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel(): void {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-between">
|
||||
<UiButton type="button" size="sm" variant="outline" @click="() => isDeletingCategory = false">
|
||||
{{ $t('ui.action.back') }}
|
||||
</UiButton>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" variant="destructive" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.delete') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
88
components/calendar/form/DeleteEvent.vue
Normal file
88
components/calendar/form/DeleteEvent.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||
|
||||
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
||||
const { isDeleteEventModalOpen, eventSkeleton } = storeToRefs(useCalendar())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
async function handleAction(): Promise<void> {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const eventTitle = eventSkeleton.value.title
|
||||
|
||||
try {
|
||||
await deleteEventFromSkeleton()
|
||||
|
||||
isDeleteEventModalOpen.value = false
|
||||
resetSkeleton()
|
||||
|
||||
toast({
|
||||
title: t("entity.calendar.event.deletedToast.title", { event: eventTitle }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.MEDIUM
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel(): void {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-between">
|
||||
<UiButton type="button" size="sm" variant="outline" @click="() => isDeleteEventModalOpen = false">
|
||||
{{ $t('ui.action.back') }}
|
||||
</UiButton>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" variant="destructive" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.delete') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
147
components/calendar/form/Update.vue
Normal file
147
components/calendar/form/Update.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
|
||||
import type { World } from "~/models/World";
|
||||
|
||||
const props = defineProps<{
|
||||
calendar: Calendar | null,
|
||||
world: World | null,
|
||||
}>()
|
||||
|
||||
const calendarSkeleton = ref<Calendar>({ ...props.calendar } as Calendar)
|
||||
|
||||
onMounted(() => {
|
||||
calendarSkeleton.value = { ...props.calendar } as Calendar
|
||||
})
|
||||
|
||||
type FormTabs = "global" | "months" | "today"
|
||||
const activeTab = ref<FormTabs>("global")
|
||||
|
||||
/**
|
||||
* === Form Validation ===
|
||||
*/
|
||||
|
||||
/** Whether the skeleton has valid month data */
|
||||
const validSkeletonMonths = computed(() => calendarSkeleton.value.months.length > 0)
|
||||
|
||||
/** Whether the skeleton has a valid name */
|
||||
const validSkeletonGeneral = computed(() => calendarSkeleton.value.name)
|
||||
|
||||
/** Whether all the data checks above are a-ok */
|
||||
const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeletonMonths.value)
|
||||
|
||||
/** Send the data to the store for validation */
|
||||
const isUpdatingCalendar = ref<boolean>(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
isUpdatingCalendar.value = true
|
||||
await $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.world?.id } })
|
||||
|
||||
emit("on-close")
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
isUpdatingCalendar.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* === Watch for name changes to display above ===
|
||||
*/
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(e: "on-changed-name", calendarName: string): void
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(e: "on-close"): void
|
||||
}>()
|
||||
|
||||
/** Hook to emit a debounced event for the changed skeleton name */
|
||||
const handleNameChange = useDebounceFn(() => {
|
||||
emit("on-changed-name", calendarSkeleton.value.name)
|
||||
}, 400)
|
||||
|
||||
function handleFormCancel() {
|
||||
emit("on-close")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="calendarSkeleton">
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<UiTabs v-model:model-value="activeTab">
|
||||
<UiTabsList class="grid w-full grid-cols-1 mb-4">
|
||||
<UiTabsTrigger value="global" class="font-bold">
|
||||
<div class="flex items-center gap-1">
|
||||
<PhWrench size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.actionDialog.tabs.general.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
<!-- <UiTabsTrigger value="months" class="font-bold">
|
||||
<div class="flex items-center gap-1">
|
||||
<PhCalendarDots size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
<UiTabsTrigger value="today" class="font-bold">
|
||||
<div class="flex items-center gap-1">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger> -->
|
||||
</UiTabsList>
|
||||
<UiTabsContent value="global" class="grid gap-4">
|
||||
<input
|
||||
id="new-calendar-name"
|
||||
v-model="calendarSkeleton.name"
|
||||
type="text"
|
||||
name="new-calendar-name"
|
||||
required
|
||||
:placeholder="$t('common.title')"
|
||||
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
|
||||
@input="handleNameChange"
|
||||
>
|
||||
|
||||
<div class="-mx-1 px-1 grid gap-3">
|
||||
<UiLabel for="new-calendar-state">
|
||||
{{ $t('ui.contentState.label') }}
|
||||
</UiLabel>
|
||||
|
||||
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
|
||||
</div>
|
||||
|
||||
<div class="-mx-1 grid gap-3">
|
||||
<UiLabel for="new-calendar-color">
|
||||
{{ $t('ui.colors.label') }}
|
||||
</UiLabel>
|
||||
|
||||
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
|
||||
</div>
|
||||
</UiTabsContent>
|
||||
<!-- <UiTabsContent value="months">
|
||||
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
|
||||
</UiTabsContent>
|
||||
<UiTabsContent value="today">
|
||||
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
|
||||
</UiTabsContent> -->
|
||||
</UiTabs>
|
||||
|
||||
<footer class="flex justify-end gap-2 mt-6">
|
||||
<UiButton type="button" variant="destructive" @click="handleFormCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
|
||||
<UiButton type="submit" :disabled="!validSkeleton || isUpdatingCalendar">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isUpdatingCalendar" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,112 +1,214 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhAlarm } from '@phosphor-icons/vue'
|
||||
import { VisuallyHidden } from 'radix-vue'
|
||||
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||
import type { APIError } from "~/models/Errors";
|
||||
|
||||
const isModalOpened = defineModel<boolean>({ default: false })
|
||||
const { resetSkeleton, updateEventFromSkeleton } = useCalendarEvents()
|
||||
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendarEvents())
|
||||
const emit = defineEmits(["event-updated"])
|
||||
|
||||
const { resetSkeleton, updateEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
||||
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
// Watch the popover state
|
||||
watch(isModalOpened, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = { ...lastActiveEvent.value }
|
||||
} else {
|
||||
resetSkeleton()
|
||||
}
|
||||
})
|
||||
async function handleAction() {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await updateEventFromSkeleton()
|
||||
|
||||
isModalOpened.value = false
|
||||
emit("event-updated")
|
||||
|
||||
toast({
|
||||
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const apiError = (err as any).data as APIError
|
||||
|
||||
apiError.data.errors.forEach((error) => {
|
||||
toast({
|
||||
title: t("entity.calendar.event.editErrors.toastTitle"),
|
||||
variant: "destructive",
|
||||
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
|
||||
duration: ToastLifetime.MEDIUM,
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
resetSkeleton()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel() {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="isModalOpened" :modal="true">
|
||||
<UiDialogContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<UiDialogTitle> Modifier l'évènement</UiDialogTitle>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-3">
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhPencilSimpleLine size="20" weight="fill" />
|
||||
|
||||
<UiDialogDescription>
|
||||
Mettre à jour les données de l'évènement
|
||||
</UiDialogDescription>
|
||||
</VisuallyHidden>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="col-span-2 ml-8">
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-title"
|
||||
v-model="eventSkeleton.title"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
placeholder="Titre de l'évènement"
|
||||
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">
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 ml-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
placeholder="Description brève de l'évènement"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
placeholder="Date de début"
|
||||
:initial-date="lastActiveEvent?.startDate"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
placeholder="Date de fin"
|
||||
:initial-date="lastActiveEvent?.endDate"
|
||||
/>
|
||||
:placeholder="$t('entity.calendar.event.title')"
|
||||
:minlength="3"
|
||||
:maxlength="120"
|
||||
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2 my-2 ml-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
:placeholder="$t('entity.addDescription')"
|
||||
:maxlength="1200"
|
||||
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<UiButton size="sm" type="submit">
|
||||
Sauvegarder
|
||||
</UiButton>
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
:placeholder="$t('entity.calendar.date.start')"
|
||||
:initial-date="lastActiveEvent?.startDate"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
:placeholder="$t('entity.calendar.date.end')"
|
||||
:initial-date="lastActiveEvent?.endDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<div class="w-1/2">
|
||||
<CalendarInputEventCategory
|
||||
v-model="eventSkeleton.category"
|
||||
:placeholder="$t('entity.category.addPrimary')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<div class="w-1/2">
|
||||
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col-span-2 mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhMapPinArea size="18" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-location"
|
||||
v-model="eventSkeleton.location"
|
||||
type="text"
|
||||
name="new-event-location"
|
||||
:placeholder="$t('entity.calendar.event.addLocation')"
|
||||
:minlength="3"
|
||||
:maxlength="160"
|
||||
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
|
||||
>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
|
||||
<PhEyeClosed v-else size="18" />
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
|
||||
<UiLabel for="new-event-visibility">
|
||||
<template v-if="!eventSkeleton.hidden">
|
||||
{{ $t('entity.calendar.event.isPublic') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.event.isHidden') }}
|
||||
</template>
|
||||
</UiLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
84
components/calendar/input/EventCategories.vue
Normal file
84
components/calendar/input/EventCategories.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
import { PhCaretDown, PhCheck } from "@phosphor-icons/vue";
|
||||
|
||||
const isPopoverOpen = ref<boolean>(false)
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const model = defineModel<Category[]>({ default: [] })
|
||||
const modelBuffer = ref<Category[]>([])
|
||||
|
||||
watch(modelBuffer.value, () => {
|
||||
model.value = [ ...modelBuffer.value ]
|
||||
})
|
||||
|
||||
const { categories: availableCategories } = useCalendar()
|
||||
|
||||
const searchTerm = ref<string>("")
|
||||
|
||||
const filteredCategories = computed(() =>
|
||||
searchTerm.value === ""
|
||||
? availableCategories
|
||||
: availableCategories.filter((category) => {
|
||||
return category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
class="relative w-full max-w-full h-fit justify-between"
|
||||
>
|
||||
<template v-if="!model.length">
|
||||
{{ props.placeholder }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<ul class="flex flex-wrap gap-1">
|
||||
<li v-for="category in model" :key="`selected-cat-${category.id}`">
|
||||
<UiBadge class="lowercase" variant="secondary">
|
||||
{{ category.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
:collision-padding="50"
|
||||
class="w-fit h-[33vh] p-0"
|
||||
>
|
||||
<UiCommand v-model="modelBuffer" v-model:searchTerm="searchTerm" :multiple="true">
|
||||
<UiCommandInput :placeholder="$t('entity.category.search')" />
|
||||
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
|
||||
<UiCommandList>
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:value="category"
|
||||
class="cursor-pointer flex justify-between items-center"
|
||||
>
|
||||
<span>
|
||||
{{ category.name }}
|
||||
</span>
|
||||
|
||||
<PhCheck v-if="model.find(cat => cat.id === category.id)" />
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</template>
|
||||
73
components/calendar/input/EventCategory.vue
Normal file
73
components/calendar/input/EventCategory.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCaretDown } from "@phosphor-icons/vue";
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
const isPopoverOpen = ref<boolean>(false)
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const model = defineModel<Category>()
|
||||
|
||||
const { categories: availableCategories } = useCalendar()
|
||||
|
||||
const searchTerm = ref<string>("")
|
||||
|
||||
function handleCatSelect() {
|
||||
isPopoverOpen.value = false
|
||||
}
|
||||
|
||||
const filteredCategories = computed(() =>
|
||||
searchTerm.value === ""
|
||||
? availableCategories
|
||||
: availableCategories.filter((category) => {
|
||||
return category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
class="w-full max-w-full justify-between"
|
||||
>
|
||||
<template v-if="!model">
|
||||
{{ props.placeholder }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ model.name }}
|
||||
</template>
|
||||
|
||||
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
:collision-padding="50"
|
||||
class="w-fit h-[33vh] p-0"
|
||||
>
|
||||
<UiCommand v-model="model" v-model:searchTerm="searchTerm">
|
||||
<UiCommandInput :placeholder="$t('entity.category.search')" />
|
||||
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
|
||||
<UiCommandList>
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:value="category"
|
||||
class="cursor-pointer"
|
||||
@select="handleCatSelect"
|
||||
>
|
||||
{{ category.name }}
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</template>
|
||||
172
components/calendar/input/MonthList.vue
Normal file
172
components/calendar/input/MonthList.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { useSortable } from "@vueuse/integrations/useSortable";
|
||||
import type { CalendarMonth } from "~/models/CalendarMonth";
|
||||
|
||||
import { PhList, PhPlus, PhTrash } from "@phosphor-icons/vue";
|
||||
|
||||
const model = defineModel<CalendarMonth[]>({ required: true })
|
||||
|
||||
/**
|
||||
* Input value for new month's name
|
||||
*/
|
||||
const monthName: Ref<string | undefined> = ref<string>()
|
||||
const monthNameRef = ref<HTMLInputElement>()
|
||||
const { focused: monthNameFocused } = useFocus(monthNameRef)
|
||||
const validMonthNameDatatypes = ["string"]
|
||||
const monthNameIsTaken = computed(() => model.value.find(m => m.name === monthName.value))
|
||||
const validMonthName = computed(() => validMonthNameDatatypes.includes(typeof monthName.value) && !monthNameIsTaken.value)
|
||||
|
||||
/**
|
||||
* Input value for new month's number of days
|
||||
*/
|
||||
const monthDays: Ref<number | undefined> = ref<number>()
|
||||
const monthDaysRef = ref<HTMLInputElement>()
|
||||
const validMonthDaysDatatypes = ["number"]
|
||||
const validMonthDays = computed(() => validMonthDaysDatatypes.includes(typeof monthDays.value) && monthDays.value && monthDays.value >= 12)
|
||||
|
||||
const validNewMonth = computed(() => validMonthDays.value && validMonthName.value)
|
||||
|
||||
/**
|
||||
* Add current month input data to the model list
|
||||
*/
|
||||
function addMonthToModel(): void {
|
||||
if (!monthDays.value || !monthName.value) return
|
||||
|
||||
// Create month object to add
|
||||
const monthToInsert: CalendarMonth = {
|
||||
name: monthName.value,
|
||||
days: monthDays.value,
|
||||
position: model.value.length
|
||||
}
|
||||
|
||||
model.value.push(monthToInsert)
|
||||
|
||||
// Reset form state
|
||||
monthName.value = ""
|
||||
monthNameFocused.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific month from the model
|
||||
*
|
||||
* @param index Index position of the month in the model
|
||||
*/
|
||||
function removeMonthFromModel(index: number) {
|
||||
if (isNaN(index)) return
|
||||
|
||||
model.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// Sortable setup
|
||||
const monthSortableList = ref<HTMLElement | null>(null)
|
||||
useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle" })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid md:grid-cols-12 gap-4 items-center">
|
||||
<div class="md:col-start-2 md:col-span-5">
|
||||
<UiInput
|
||||
id="new-month-name"
|
||||
ref="monthNameRef"
|
||||
v-model="monthName"
|
||||
type="text"
|
||||
name="newMonthName"
|
||||
:placeholder="$t('entity.calendar.months.inputName')"
|
||||
:class="cn({ 'border-red-600': monthNameIsTaken })"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-5">
|
||||
<UiInput
|
||||
id="new-month-days"
|
||||
ref="monthDaysRef"
|
||||
v-model="monthDays"
|
||||
type="number"
|
||||
name="newMonthName"
|
||||
:placeholder="$t('entity.calendar.months.daysNb')"
|
||||
min="0"
|
||||
step="1"
|
||||
class="invalid:border-red-600"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-1">
|
||||
<UiButton size="icon" class="rounded-full h-8 w-8" :disabled="!validNewMonth" @click.prevent="addMonthToModel">
|
||||
<PhPlus size="17"/>
|
||||
</UiButton>
|
||||
</div>
|
||||
<div class="md:col-span-full">
|
||||
<div
|
||||
class="border-[1px] border-border p-4 rounded-sm max-h-80 overflow-y-auto"
|
||||
:class="model.length ? 'md:grid md:grid-cols-12 md:gap-4 md:items-center' : ''"
|
||||
>
|
||||
<div v-if="model.length" class="hidden md:block col-span-1">
|
||||
<ul class="grid gap-y-4 justify-center">
|
||||
<li v-for="(m, i) in model" :key="`num-${m.name}`">
|
||||
<UiButton size="icon" variant="secondary" class="h-8 w-8 rounded-full">
|
||||
<span class="font-bold text-sm">{{ i + 1 }}</span>
|
||||
</UiButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-11">
|
||||
<ul ref="monthSortableList" class="grid gap-y-2" :class="model.length ? 'visible' : 'absolute invisible'">
|
||||
<template v-if="model.length">
|
||||
<li v-for="(m, i) in model" :key="m.name" class="grid md:grid-cols-12 gap-4 md:items-center text-slate-900 bg-slate-200 rounded-md">
|
||||
<div class="md:col-span-1 text-right duration-200 ease-out transition transform origin-top-right">
|
||||
<UiButton type="button" variant="ghost" size="icon" class="handle rounded-full h-8 w-8">
|
||||
<PhList size="17" />
|
||||
</UiButton>
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<div class="font-bold md:pl-2">
|
||||
{{ m.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<UiInput
|
||||
:id="`month-days-n${i}`"
|
||||
v-model="m.days"
|
||||
class="bg-transparent border-none"
|
||||
type="number"
|
||||
:name="`monthDays-n${i}`"
|
||||
:placeholder="$t('entity.calendar.months.daysNb')"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-1">
|
||||
{{ $t('entity.calendar.months.daysMaybePlural') }}
|
||||
</div>
|
||||
|
||||
<div class="md:col-start-12">
|
||||
<UiTooltipProvider>
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton type="button" variant="ghost" size="icon" class="rounded-full h-8 w-8" @click="removeMonthFromModel(i)">
|
||||
<PhTrash size="17" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('entity.calendar.months.deleteOne', { month: m.name }) }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="!model.length">
|
||||
<p class="col-span-12 text-lg text-center italic opacity-50">
|
||||
{{ $t('entity.calendar.months.none') }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from '~/models/Date';
|
||||
import type { RPGDate } from "~/models/Date";
|
||||
|
||||
import {
|
||||
PhXCircle
|
||||
} from '@phosphor-icons/vue'
|
||||
} from "@phosphor-icons/vue"
|
||||
|
||||
const model = defineModel<RPGDate | null>()
|
||||
|
||||
@@ -27,7 +27,7 @@ const inputPlaceholder = computed<string>(() => {
|
||||
return props.placeholder
|
||||
}
|
||||
|
||||
return ''
|
||||
return ""
|
||||
})
|
||||
|
||||
const initialDateValue = props.initialDate ? { ...props.initialDate } : null
|
||||
@@ -37,7 +37,7 @@ const monthTitle = computed<string>(() => {
|
||||
return getMonthName(model.value.month)
|
||||
}
|
||||
|
||||
return ''
|
||||
return ""
|
||||
})
|
||||
|
||||
const hasValue = computed<boolean>(() => !!model.value)
|
||||
@@ -117,7 +117,7 @@ function handleValueReset() {
|
||||
v-if="model"
|
||||
align="start"
|
||||
side="bottom"
|
||||
class="bg-slate-900 border-slate-800"
|
||||
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
>
|
||||
<div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
|
||||
<UiSelect @update:model-value="handleMonthChange">
|
||||
@@ -151,8 +151,11 @@ function handleValueReset() {
|
||||
<button
|
||||
v-for="day in monthData.days"
|
||||
:key="`popover-day-grid-${id}-${day}`"
|
||||
class="aspect-square rounded-full text-[.8em] transition-colors hover:bg-slate-700"
|
||||
:class="{'bg-blue-500 hover:bg-blue-700': day === model.day}"
|
||||
class="aspect-square rounded-full text-[.8em] transition-colors"
|
||||
:class="{
|
||||
'hover:bg-indigo-200 dark:hover:bg-indigo-700': day !== model.day,
|
||||
'bg-indigo-500 hover:bg-indigo-700 text-white': day === model.day
|
||||
}"
|
||||
@click="setModelDay(day)"
|
||||
>
|
||||
{{ day }}
|
||||
|
||||
71
components/calendar/input/TodaySelect.vue
Normal file
71
components/calendar/input/TodaySelect.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarMonth } from "~/models/CalendarMonth";
|
||||
import type { RPGDate } from "~/models/Date";
|
||||
|
||||
import { PhCalendarBlank } from "@phosphor-icons/vue";
|
||||
|
||||
const model = defineModel<RPGDate>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
availableMonths: CalendarMonth[]
|
||||
}>()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param e The name of today's month
|
||||
*/
|
||||
function setTodayMonth(e: string) {
|
||||
model.value.month = e
|
||||
}
|
||||
|
||||
// When the model changes, get the month index from the month name
|
||||
watch(model.value, (n, _o) => {
|
||||
// If the month value is already an index, return early
|
||||
if (!isNaN(+model.value.month)) return
|
||||
|
||||
const monthId = props.availableMonths.findIndex((m) => m.name === n.month)
|
||||
|
||||
if (monthId !== -1) {
|
||||
model.value.month = monthId + 1
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2 items-stretch">
|
||||
<UiInput id="new-month-current-day" v-model="model.day" type="number" name="newMonthCurrentDay" :placeholder="$t('entity.calendar.months.daysNameSingular')" min="1" step="1" class="invalid:border-red-600" />
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child :disabled="props.availableMonths.length < 1">
|
||||
<UiButton size="sm" variant="secondary">
|
||||
<PhCalendarBlank size="18" weight="fill" />
|
||||
|
||||
<template v-if="props.availableMonths.length < 1">
|
||||
{{ $t('entity.calendar.months.noneAvailable') }}
|
||||
</template>
|
||||
<template v-else-if="model.month && typeof model.month === 'number'">
|
||||
{{ props.availableMonths[model.month - 1].name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.months.chooseOne') }}
|
||||
</template>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
|
||||
<UiDropdownMenuLabel>
|
||||
{{ $t('entity.calendar.months.available') }}
|
||||
</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem
|
||||
v-for="m in props.availableMonths"
|
||||
:key="m.name"
|
||||
@click="setTodayMonth(m.name)"
|
||||
>
|
||||
{{ m.name }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiInput id="new-month-current-day" v-model="model.year" type="number" name="newMonthCurrentYear" :placeholder="$t('entity.calendar.years.nameSingular')" step="1" class="invalid:border-red-600" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,22 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
|
||||
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
const { isReadOnly } = storeToRefs(useCalendar())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="pt-4 border-slate-700 border-b-[1px]">
|
||||
<div class="px-6 flex justify-between">
|
||||
<header class="mt-2 grid gap-4 border-border border-b-[1px]">
|
||||
<div class="px-8 flex items-center justify-between gap-2">
|
||||
<menu class="flex items-center gap-2">
|
||||
<li class="flex items-center">
|
||||
<CalendarMenuToday />
|
||||
<li v-if="!isReadOnly">
|
||||
<LazyCalendarDialogQuickCreateEvent />
|
||||
</li>
|
||||
<li>
|
||||
<CalendarMenuNav />
|
||||
<CalendarMenuToday />
|
||||
</li>
|
||||
<li class="ml-6">
|
||||
<li class="ml-4">
|
||||
<CalendarCurrentDate />
|
||||
</li>
|
||||
</menu>
|
||||
@@ -25,16 +26,21 @@ const { revealAdvancedSearch } = useCalendar()
|
||||
<li>
|
||||
<UiButton search-slash @click="revealAdvancedSearch()">
|
||||
<PhMagnifyingGlass size="20" weight="light" />
|
||||
Recherche avancée
|
||||
<span>
|
||||
{{ $t('entity.advancedSearch.title') }}
|
||||
</span>
|
||||
</UiButton>
|
||||
</li>
|
||||
<li>
|
||||
<CalendarSwitch />
|
||||
<CalendarCategoriesCTA />
|
||||
</li>
|
||||
<li>
|
||||
<CalendarOptionsCTA />
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div class="ml-6">
|
||||
<div class="ml-8">
|
||||
<CalendarMenuSubnav />
|
||||
</div>
|
||||
</header>
|
||||
230
components/calendar/menu/MenuSubnav.vue
Normal file
230
components/calendar/menu/MenuSubnav.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCaretDoubleLeft, PhCaretDoubleRight, PhCaretLeft, PhCaretRight } from "@phosphor-icons/vue"
|
||||
|
||||
const { currentDate } = useCalendar()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface DirectionLabels {
|
||||
pastFar: string
|
||||
pastNear: string
|
||||
futureNear: string
|
||||
futureFar: string
|
||||
}
|
||||
|
||||
const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } =
|
||||
useCalendar()
|
||||
|
||||
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case "month":
|
||||
return {
|
||||
pastFar: t("entity.calendar.years.prevSingular"),
|
||||
pastNear: t("entity.calendar.months.prevSingular"),
|
||||
futureNear: t("entity.calendar.months.nextSingular"),
|
||||
futureFar: t("entity.calendar.years.nextSingular")
|
||||
}
|
||||
|
||||
case "year":
|
||||
return {
|
||||
pastFar: t("entity.calendar.decades.prevSingular"),
|
||||
pastNear: t("entity.calendar.years.prevSingular"),
|
||||
futureNear: t("entity.calendar.years.nextSingular"),
|
||||
futureFar: t("entity.calendar.decades.nextSingular")
|
||||
}
|
||||
|
||||
case "decade":
|
||||
return {
|
||||
pastFar: t("entity.calendar.centuries.prevSingular"),
|
||||
pastNear: t("entity.calendar.decades.prevSingular"),
|
||||
futureNear: t("entity.calendar.decades.nextSingular"),
|
||||
futureFar: t("entity.calendar.centuries.nextSingular")
|
||||
}
|
||||
|
||||
case "century":
|
||||
default:
|
||||
return {
|
||||
pastFar: t("entity.calendar.millenias.prevSingular"),
|
||||
pastNear: t("entity.calendar.centuries.prevSingular"),
|
||||
futureNear: t("entity.calendar.centuries.nextSingular"),
|
||||
futureFar: t("entity.calendar.millenias.nextSingular")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function toPastFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case "month":
|
||||
decrementViewYear()
|
||||
break
|
||||
|
||||
case "year":
|
||||
decrementViewYear(10)
|
||||
break
|
||||
|
||||
case "decade":
|
||||
decrementViewYear(100)
|
||||
break
|
||||
|
||||
case "century":
|
||||
default:
|
||||
decrementViewYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toPastNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case "month":
|
||||
decrementViewMonth()
|
||||
break
|
||||
|
||||
case "year":
|
||||
decrementViewYear()
|
||||
break
|
||||
|
||||
case "decade":
|
||||
decrementViewYear(10)
|
||||
break
|
||||
|
||||
case "century":
|
||||
default:
|
||||
decrementViewYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case "month":
|
||||
incrementViewMonth()
|
||||
break
|
||||
|
||||
case "year":
|
||||
incrementViewYear()
|
||||
break
|
||||
|
||||
case "decade":
|
||||
incrementViewYear(10)
|
||||
break
|
||||
|
||||
case "century":
|
||||
default:
|
||||
incrementViewYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case "month":
|
||||
incrementViewYear()
|
||||
break
|
||||
|
||||
case "year":
|
||||
incrementViewYear(10)
|
||||
break
|
||||
|
||||
case "decade":
|
||||
incrementViewYear(100)
|
||||
break
|
||||
|
||||
case "century":
|
||||
default:
|
||||
incrementViewYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div class="grid items-end w-40 px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors">
|
||||
<ClientOnly>
|
||||
<span>{{ currentDate.currentDateTitle }}</span>
|
||||
|
||||
<template #fallback>
|
||||
<span class="inline-block">
|
||||
<UiSkeleton class="h-[19px] w-full rounded-sm" />
|
||||
</span>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<div>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none border-b-0"
|
||||
@click="toPastFar()"
|
||||
>
|
||||
<PhCaretDoubleLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
<div>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none border-b-0"
|
||||
@click="toPastNear()"
|
||||
>
|
||||
<PhCaretLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
<div>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none border-b-0"
|
||||
@click="toFutureNear()"
|
||||
>
|
||||
<PhCaretRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
<div>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none border-b-0"
|
||||
@click="toFutureFar()"
|
||||
>
|
||||
<PhCaretDoubleRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
26
components/calendar/menu/MenuToday.vue
Normal file
26
components/calendar/menu/MenuToday.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
|
||||
|
||||
const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
|
||||
|
||||
const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
|
||||
return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton size="sm" variant="secondary" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
|
||||
{{ $t('entity.calendar.date.today') }}
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ defaultDateFormatted }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</template>
|
||||
@@ -2,50 +2,54 @@
|
||||
import {
|
||||
isCharacter,
|
||||
type Character,
|
||||
} from '@/models/Characters'
|
||||
import type { RPGDateOrder } from '@/models/Date'
|
||||
} from "@/models/Characters"
|
||||
import type { RPGDateOrder } from "@/models/Date"
|
||||
import {
|
||||
isCalendarEvent,
|
||||
type CalendarEvent,
|
||||
} from '~/models/CalendarEvent'
|
||||
import { useCharacters } from '@/stores/CharacterStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { capitalize } from '@/utils/Strings'
|
||||
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { searchUnifier, type SearchMode } from '../SearchMode'
|
||||
} from "~/models/CalendarEvent"
|
||||
import { capitalize } from "@/utils/Strings"
|
||||
import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { searchUnifier, type SearchMode } from "../SearchMode"
|
||||
|
||||
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import { PhCaretDown, PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from "@phosphor-icons/vue"
|
||||
import {
|
||||
ComboboxAnchor,
|
||||
ComboboxInput,
|
||||
ComboboxPortal,
|
||||
ComboboxRoot,
|
||||
ComboboxTrigger,
|
||||
VisuallyHidden
|
||||
} from 'radix-vue'
|
||||
} from "radix-vue"
|
||||
|
||||
import SearchList from './lists/SearchList.vue'
|
||||
import type { Category } from '~/models/Category'
|
||||
import SearchList from "./lists/SearchList.vue"
|
||||
import type { Category } from "~/models/Category"
|
||||
|
||||
const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar())
|
||||
const { characters } = storeToRefs(useCharacters())
|
||||
const { allEvents } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const modalOpen = defineModel<boolean>({ default: false })
|
||||
const searchInput = shallowRef<HTMLInputElement>()
|
||||
useFocus(searchInput, { initialValue: true })
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
const searchQuery = ref<string>("")
|
||||
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
|
||||
|
||||
const selectedEntity = useStorage('se', 'events' as SearchMode)
|
||||
// If the query changes, resets the pagination
|
||||
// This prevents active page to be greater than the search results
|
||||
watch(searchQuery, resetPage)
|
||||
|
||||
const selectedEntity = useStorage("se", "events" as SearchMode)
|
||||
|
||||
// Order
|
||||
const selectedOrder = ref<RPGDateOrder>('asc')
|
||||
const selectedOrder = ref<RPGDateOrder>("asc")
|
||||
function setOrderAsc() {
|
||||
selectedOrder.value = 'asc'
|
||||
selectedOrder.value = "asc"
|
||||
resetPage()
|
||||
}
|
||||
|
||||
function setOrderDesc() {
|
||||
selectedOrder.value = 'desc'
|
||||
selectedOrder.value = "desc"
|
||||
resetPage()
|
||||
}
|
||||
|
||||
@@ -69,9 +73,9 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
|
||||
// Assign data to loop over and filter
|
||||
// They are assigned this way for readability
|
||||
if (selectedEntity.value === 'events') {
|
||||
if (selectedEntity.value === "events") {
|
||||
dataToFilter = allEvents.value
|
||||
} else if (selectedEntity.value === 'characters') {
|
||||
} else if (selectedEntity.value === "characters") {
|
||||
dataToFilter = characters.value
|
||||
} else {
|
||||
dataToFilter = [...allEvents.value, ...characters.value]
|
||||
@@ -84,15 +88,15 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
// Filter calendar events
|
||||
if (isCalendarEvent(item)) {
|
||||
const queryString = new String(searchQuery.value)
|
||||
.replace(searchUnifier, '')
|
||||
.replace(searchUnifier, "")
|
||||
.toLocaleLowerCase()
|
||||
|
||||
const hitTitle = item.title
|
||||
.replace(searchUnifier, '')
|
||||
.replace(searchUnifier, "")
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
const hitDesc = item.description
|
||||
?.replace(searchUnifier, '')
|
||||
?.replace(searchUnifier, "")
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
@@ -113,7 +117,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat as Category)
|
||||
return allCategories.findIndex((c) => c.name === selectedCat.name) !== -1
|
||||
})
|
||||
|
||||
return (hitTitle || hitDesc) && hitCategories
|
||||
@@ -122,11 +126,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
// Filter characters
|
||||
if (isCharacter(item)) {
|
||||
const queryString = new String(searchQuery.value)
|
||||
.replace(searchUnifier, '')
|
||||
.replace(searchUnifier, "")
|
||||
.toLocaleLowerCase()
|
||||
|
||||
const hitTitle = item.name
|
||||
.replace(searchUnifier, '')
|
||||
.replace(searchUnifier, "")
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
@@ -161,7 +165,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
* Removes the search query, resets the pagination and removes all selected categories
|
||||
*/
|
||||
function resetSearch() {
|
||||
searchQuery.value = ''
|
||||
searchQuery.value = ""
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
@@ -170,14 +174,14 @@ function resetSearch() {
|
||||
* Opens the search Uidialog
|
||||
*/
|
||||
function openUiDialog() {
|
||||
modalOpen.value = true
|
||||
isAdvancedSearchOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the search Uidialog
|
||||
*/
|
||||
function closeUiDialog() {
|
||||
modalOpen.value = false
|
||||
isAdvancedSearchOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,28 +207,53 @@ watch([currentPage, selectedEntity], () => {
|
||||
})
|
||||
|
||||
// Compute categories based on current selectedEntity
|
||||
const currentCategories = computed(() => {
|
||||
return []
|
||||
})
|
||||
const currentCategories = computed(() => categories.value)
|
||||
|
||||
const selectedCategories = ref<(Category)[]>([])
|
||||
const categoryFilterOpened = ref<boolean>(false)
|
||||
const searchCategory = ref<string>('')
|
||||
const searchCategory = ref<string>("")
|
||||
|
||||
const filteredCategories = computed(() =>
|
||||
currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
|
||||
)
|
||||
const filteredCategories = computed(() => {
|
||||
// Display original data
|
||||
if (!currentCategories.value) return []
|
||||
|
||||
// If current categories are selected, ignore them
|
||||
if (!searchCategory.value) return currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
|
||||
|
||||
// If we also have a query to filter them by, do that
|
||||
return currentCategories.value.filter((i) => {
|
||||
return !selectedCategories.value.includes(i) && i.name.toLocaleLowerCase().includes(searchCategory.value.toLocaleLowerCase())
|
||||
})
|
||||
})
|
||||
|
||||
// Reactivity rules for category searches
|
||||
const categoryInput = ref(null)
|
||||
const categoryInputValue = ref<string>("")
|
||||
const { focused: categoryInputFocused } = useFocus(categoryInput)
|
||||
|
||||
watch(categoryInputFocused, (isFocused) => {
|
||||
categoryFilterOpened.value = isFocused
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles the category selections from the TagInput component
|
||||
*
|
||||
* @param e Radix Change Event
|
||||
*/
|
||||
function handleCategorySelect(e: (Category)) {
|
||||
if (typeof e === 'string') {
|
||||
searchCategory.value = ''
|
||||
selectedCategories.value.push(e)
|
||||
function handleCategorySelect(e: Category) {
|
||||
searchCategory.value = ""
|
||||
selectedCategories.value.push(e)
|
||||
|
||||
if (filteredCategories.value.length === 0) {
|
||||
categoryFilterOpened.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a category from selection from the TagInput component
|
||||
*/
|
||||
function handleCategoryUnselect(e: Category) {
|
||||
selectedCategories.value.splice(selectedCategories.value.findIndex(sc => sc.name === e.name), 1)
|
||||
|
||||
if (filteredCategories.value.length === 0) {
|
||||
categoryFilterOpened.value = false
|
||||
@@ -233,7 +262,7 @@ function handleCategorySelect(e: (Category)) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()">
|
||||
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
|
||||
<UiDialogContent
|
||||
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
|
||||
:class="{
|
||||
@@ -241,11 +270,11 @@ function handleCategorySelect(e: (Category)) {
|
||||
}"
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<UiDialogTitle> Recherche avancée </UiDialogTitle>
|
||||
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
|
||||
</VisuallyHidden>
|
||||
<VisuallyHidden>
|
||||
<UiDialogDescription>
|
||||
Rechercher les données disponibles sur le calendrier
|
||||
{{ $t('entity.advancedSearch.title') }}
|
||||
</UiDialogDescription>
|
||||
</VisuallyHidden>
|
||||
|
||||
@@ -254,9 +283,10 @@ function handleCategorySelect(e: (Category)) {
|
||||
<div class="relative w-full h-fit">
|
||||
<UiInput
|
||||
id="search"
|
||||
ref="searchInput"
|
||||
v-model:model-value="searchQuery"
|
||||
type="text"
|
||||
placeholder="Rechercher le calendrier"
|
||||
:placeholder="$t('entity.advancedSearch.ctaPlaceholder')"
|
||||
class="pl-10 py-6 text-lg"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@@ -265,7 +295,7 @@ function handleCategorySelect(e: (Category)) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between gap-8">
|
||||
<div>
|
||||
<UiToggleGroup
|
||||
v-model="selectedEntity"
|
||||
@@ -274,54 +304,59 @@ function handleCategorySelect(e: (Category)) {
|
||||
@update:model-value="handleEntitySwitch()"
|
||||
>
|
||||
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
|
||||
Évènements
|
||||
</UiToggleGroupItem>
|
||||
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
|
||||
Personnages
|
||||
{{ $t('entity.calendar.event.namePlural') }}
|
||||
</UiToggleGroupItem>
|
||||
<!-- Not used for now -->
|
||||
<!-- <UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
|
||||
{{ $t('entity.character.namePlural') }}
|
||||
</UiToggleGroupItem> -->
|
||||
</UiToggleGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<UiTagsInput class="px-0 gap-0 w-72">
|
||||
<div class="flex gap-2 flex-wrap items-center px-3">
|
||||
<div class="grow flex justify-end items-center gap-1">
|
||||
<UiTagsInput class="grow px-0 gap-y-1 w-80">
|
||||
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
|
||||
<UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name">
|
||||
<UiTagsInputItemText class="capitalize" />
|
||||
<UiTagsInputItemDelete />
|
||||
<UiTagsInputItemText class="capitalize cursor-pointer" @click="handleCategoryUnselect(item)" />
|
||||
</UiTagsInputItem>
|
||||
</div>
|
||||
|
||||
<ComboboxRoot
|
||||
v-model="selectedCategories"
|
||||
v-model:open="categoryFilterOpened"
|
||||
v-model:searchTerm="searchCategory"
|
||||
class="w-full"
|
||||
v-model:search-term="searchCategory"
|
||||
class="grow flex items-center gap-y-1 pr-2"
|
||||
>
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxInput placeholder="Catégories" as-child>
|
||||
<ComboboxInput :placeholder="$t('entity.category.namePlural')" as-child>
|
||||
<UiTagsInputInput
|
||||
class="w-full px-3"
|
||||
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
|
||||
ref="categoryInput"
|
||||
v-model="categoryInputValue"
|
||||
class="min-w-16 px-3"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</ComboboxInput>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxTrigger>
|
||||
<PhCaretDown size="16" />
|
||||
</ComboboxTrigger>
|
||||
|
||||
<ComboboxPortal :to="'#searchForm'">
|
||||
<UiCommandList
|
||||
position="popper"
|
||||
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-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"
|
||||
>
|
||||
<UiCommandEmpty />
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="cat in filteredCategories"
|
||||
:key="cat"
|
||||
:key="cat.name"
|
||||
:value="cat"
|
||||
@select.prevent="handleCategorySelect(cat)"
|
||||
>
|
||||
{{ capitalize(cat) }}
|
||||
{{ capitalize(cat.name) }}
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
@@ -341,7 +376,7 @@ function handleCategorySelect(e: (Category)) {
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Plus ancien</p>
|
||||
<p>{{ $t('entity.advancedSearch.older') }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
@@ -358,7 +393,7 @@ function handleCategorySelect(e: (Category)) {
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Plus récent</p>
|
||||
<p>{{ $t('entity.advancedSearch.newer') }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Character } from '@/models/Characters'
|
||||
import type { RPGDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import type { Character } from "@/models/Characters"
|
||||
import type { RPGDate } from "@/models/Date"
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PhArrowSquareOut, PhPlant, PhSkull } from '@phosphor-icons/vue'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { PhArrowSquareOut, PhPlant, PhSkull } from "@phosphor-icons/vue"
|
||||
|
||||
const props = defineProps<{
|
||||
character: Character
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'query:date-jump', payload: RPGDate): void
|
||||
(e: "query:date-jump", payload: RPGDate): void
|
||||
}>()
|
||||
|
||||
const { getFormattedDateTitle } = useCalendar()
|
||||
@@ -21,7 +21,7 @@ console.log(props.character.birth)
|
||||
</script>
|
||||
|
||||
<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="flex gap-2">
|
||||
<h2 class="font-bold">
|
||||
@@ -37,7 +37,7 @@ console.log(props.character.birth)
|
||||
</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">
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
|
||||
@@ -1,56 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/CalendarEvent'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { RPGDate } from "@/models/Date"
|
||||
import type { CalendarEvent } from "@/models/CalendarEvent"
|
||||
|
||||
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm } from '@phosphor-icons/vue'
|
||||
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue"
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'query:date-jump', payload: RPGDate): void
|
||||
(e: "query:date-jump", payload: RPGDate): void
|
||||
}>()
|
||||
|
||||
const { getRelativeString } = useCalendar()
|
||||
|
||||
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||
const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar()
|
||||
|
||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||
const dateDuration: string | null = props.event.endDate
|
||||
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||
: null
|
||||
const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
|
||||
:class="{
|
||||
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category?.name === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category?.name === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category?.name === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category?.name === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category?.name === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category?.name === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category?.name === 'joueurs',
|
||||
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category?.name === 'inauguration',
|
||||
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category?.name === 'invention',
|
||||
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category?.name === 'science',
|
||||
'text-slate-900 bg-white hover:bg-yellow-200': event.category?.name === 'bénédiction',
|
||||
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category?.name === 'découverte',
|
||||
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category?.name === 'exploration',
|
||||
'text-white bg-amber-600 hover:bg-amber-700': event.category?.name === 'construction',
|
||||
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category?.name === 'arcanologie',
|
||||
'text-white bg-rose-600 hover:bg-rose-700': event.category?.name === 'criminalité',
|
||||
'text-white bg-stone-600 hover:bg-stone-700': event.category?.name === 'scandale',
|
||||
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category?.name === 'commerce'
|
||||
}"
|
||||
class="event-callout group relative block w-full text-left py-3 px-4 cursor-pointer transition-colors"
|
||||
:class="cn(
|
||||
event.category ? `element-${event.category.color}` : '',
|
||||
{
|
||||
'pt-4 is-hidden': event.hidden,
|
||||
})"
|
||||
@click="$emit('query:date-jump', event.startDate)"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="font-bold">
|
||||
<div class="flex gap-2 items-center mb-1">
|
||||
<h2 class="font-bold text-lg underline-offset-4 group-hover:underline">
|
||||
{{ event.title }}
|
||||
</h2>
|
||||
<div v-if="event.wiki">
|
||||
@@ -63,27 +43,54 @@ const dateDuration: string | null = props.event.endDate
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<div class="flex gap-2 items-center justify-between mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||
<p class="font-semibold text-sm opacity-75">
|
||||
{{ getFormattedDateTitle(event.startDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||
<p class="font-semibold text-sm opacity-75">
|
||||
{{
|
||||
$t('entity.calendar.date.fromTo',
|
||||
{
|
||||
startDate: getFormattedDateTitle(event.startDate, true),
|
||||
endDate: getFormattedDateTitle(event.endDate, true)
|
||||
}
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<div v-if="event.hidden" class="flex justify-end">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiBadge class="flex gap-1">
|
||||
<PhEye size="16" weight="fill" /> {{ $t('entity.calendar.event.isHidden') }}
|
||||
</UiBadge>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('entity.calendar.event.hiddenTooltip') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-1 flex gap-x-2 items-center">
|
||||
<template v-if="event.location">
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhMapPinArea size="16" weight="fill" /> {{ event.location }}
|
||||
</p>
|
||||
</template>
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||
</p>
|
||||
<template v-if="dateDuration">
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
@@ -91,13 +98,13 @@ const dateDuration: string | null = props.event.endDate
|
||||
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||
<UiBadge class="mix-blend-luminosity font-bold bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
|
||||
{{ event.category?.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat.id">
|
||||
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||
<UiBadge class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
|
||||
{{ cat.name }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { isCharacter, type Character } from '@/models/Characters'
|
||||
import type { RPGDate, RPGDateOrder } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { computed } from 'vue'
|
||||
import { isCalendarEvent, type CalendarEvent } from '~/models/CalendarEvent'
|
||||
import type { SearchMode } from '../../SearchMode'
|
||||
import { isCharacter, type Character } from "@/models/Characters"
|
||||
import type { RPGDate, RPGDateOrder } from "@/models/Date"
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
import { computed } from "vue"
|
||||
import { isCalendarEvent, type CalendarEvent } from "~/models/CalendarEvent"
|
||||
import type { SearchMode } from "../../SearchMode"
|
||||
|
||||
import CharacterCallout from './CharacterCallout.vue'
|
||||
import EventCallout from './EventCallout.vue'
|
||||
import CharacterCallout from "./CharacterCallout.vue"
|
||||
import EventCallout from "./EventCallout.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
results: (Character | CalendarEvent)[]
|
||||
@@ -18,7 +18,7 @@ const props = defineProps<{
|
||||
limit?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['jumpedToDate'])
|
||||
const emit = defineEmits(["jumpedToDate"])
|
||||
|
||||
const { jumpToDate, compareDates } = useCalendar()
|
||||
|
||||
@@ -26,7 +26,7 @@ function handleJumpToDate(date?: RPGDate) {
|
||||
if (!date) return
|
||||
|
||||
jumpToDate(date)
|
||||
emit('jumpedToDate')
|
||||
emit("jumpedToDate")
|
||||
}
|
||||
|
||||
// Initial sorting of the results
|
||||
@@ -60,7 +60,7 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="grid gap-4">
|
||||
<ul class="grid gap-4 pl-8 pr-12">
|
||||
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
|
||||
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/CalendarEvent'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, type ComputedRef } from 'vue'
|
||||
import type { RPGDate } from "@/models/Date"
|
||||
import type { CalendarEvent } from "@/models/CalendarEvent"
|
||||
import { useElementBounding } from "@vueuse/core"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { computed, ref, type ComputedRef } from "vue"
|
||||
|
||||
import CalendarEventButton from '../../CalendarEvent.vue'
|
||||
import CalendarEventButton from "../../event/Event.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
date: RPGDate
|
||||
@@ -18,8 +16,7 @@ const calendarTile = ref()
|
||||
const calendarEventsList = ref()
|
||||
|
||||
const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
|
||||
|
||||
/**
|
||||
* All events with a startDate / endDate that starts or ends on the tile
|
||||
@@ -76,10 +73,10 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
<template>
|
||||
<div
|
||||
ref="calendarTile"
|
||||
class="tile relative text-xs p-2 border-slate-700"
|
||||
class="tile relative text-xs p-2"
|
||||
:class="{
|
||||
'text-slate-500': props.faded,
|
||||
'text-slate-300': !props.faded
|
||||
'text-slate-300 dark:text-slate-500': props.faded,
|
||||
'text-slate-500 dark:text-slate-300': !props.faded
|
||||
}"
|
||||
>
|
||||
<button
|
||||
@@ -88,10 +85,10 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
>
|
||||
<ClientOnly>
|
||||
<span
|
||||
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-slate-800"
|
||||
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-indigo-300 dark:group-hover:border-indigo-700"
|
||||
:class="{
|
||||
'bg-slate-800': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-blue-500': isSelectedDate
|
||||
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-indigo-500': isSelectedDate
|
||||
}"
|
||||
>
|
||||
{{ date.day }}
|
||||
@@ -108,7 +105,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
}"
|
||||
>
|
||||
<TransitionGroup name="event">
|
||||
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
|
||||
<li v-for="event in eventsToDisplay" :key="event.id" class="grid w-full pointer-events-auto">
|
||||
<CalendarEventButton :event :tile-date="date" />
|
||||
</li>
|
||||
</TransitionGroup>
|
||||
@@ -146,27 +143,26 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
</ClientOnly>
|
||||
|
||||
<ClientOnly>
|
||||
<CalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
<LazyCalendarDialogCreateEvent v-if="!isReadOnly" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// .tile are the direct children of the grid
|
||||
// We need to get, between each element, a 1px solid line to separate them, for columns and rows
|
||||
.tile {
|
||||
&:not(:nth-child(10n)) {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
&:nth-child(n + 11) {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
border-right-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.event-enter-active {
|
||||
transition: all 120ms ease-in-out;
|
||||
}
|
||||
.event-move,
|
||||
.event-leave-active {
|
||||
transition: all 180ms ease-in-out;
|
||||
position: absolute;
|
||||
transition: all 200ms ease-in-out;
|
||||
}
|
||||
.event-enter-from,
|
||||
.event-leave-to {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
import { useThrottleFn } from "@vueuse/core"
|
||||
|
||||
const { currentDate, decrementMonth, incrementMonth } = useCalendar()
|
||||
const { currentDate, decrementViewMonth, incrementViewMonth } = useCalendar()
|
||||
const { currentMonthData } = storeToRefs(useCalendar())
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
@@ -15,24 +15,26 @@ function handleWheel(e: WheelEvent) {
|
||||
}
|
||||
|
||||
const moveCalendarLeft = useThrottleFn(() => {
|
||||
decrementMonth()
|
||||
decrementViewMonth()
|
||||
}, 100)
|
||||
|
||||
const moveCalendarRight = useThrottleFn(() => {
|
||||
incrementMonth()
|
||||
incrementViewMonth()
|
||||
}, 100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-10" @wheel="handleWheel">
|
||||
<CalendarStateMonthlyDayTile
|
||||
v-for="day in currentMonthData.days"
|
||||
:key="`layout-month-grid-${day}`"
|
||||
:date="{
|
||||
day: day,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}"
|
||||
/>
|
||||
<template v-if="currentMonthData">
|
||||
<CalendarStateMonthlyDayTile
|
||||
v-for="day in currentMonthData?.days"
|
||||
:key="`layout-month-grid-${day}`"
|
||||
:date="{
|
||||
day: day,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
import type { RPGDate } from "@/models/Date"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { computed, type ComputedRef } from "vue"
|
||||
|
||||
const { currentDate, defaultDate, selectDate } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||
const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar()
|
||||
const { selectedDate, currentEvents } = storeToRefs(useCalendar())
|
||||
|
||||
const props = defineProps<{
|
||||
monthNumber: number
|
||||
dayNumber: number
|
||||
}>()
|
||||
|
||||
const { areDatesIdentical } = useCalendar()
|
||||
|
||||
const tileDate: ComputedRef<RPGDate> = computed(() => {
|
||||
return {
|
||||
day: props.dayNumber,
|
||||
@@ -45,14 +40,14 @@ const hasAtLeastOneEvent = computed<boolean>(() => {
|
||||
<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="{
|
||||
'hover:bg-slate-800 hover:border-slate-800': !isDefaultDate && !isSelectedDate,
|
||||
'bg-slate-800 hover:bg-slate-600 hover:border-slate-600': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-blue-500 hover:bg-blue-600 hover:border-blue-600': isSelectedDate,
|
||||
'text-slate-500 hover:border-indigo-300 hover:text-slate-900': !isDefaultDate && !isSelectedDate,
|
||||
'font-semibold text-white bg-slate-600 dark:bg-slate-800 hover:border-indigo-400': isDefaultDate && !isSelectedDate,
|
||||
'font-semibold text-white bg-indigo-500 hover:bg-indigo-600 hover:border-indigo-500': isSelectedDate,
|
||||
'after:bg-green-600': hasAtLeastOneEvent,
|
||||
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate
|
||||
}"
|
||||
@click="selectDate(tileDate)"
|
||||
>
|
||||
<span ref="tileRef" class="text-slate-300 text-[.85em]">{{ dayNumber }}</span>
|
||||
<span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { useCalendar } from "@/stores/CalendarStore"
|
||||
import { useThrottleFn } from "@vueuse/core"
|
||||
|
||||
const { decrementYear, incrementYear } = useCalendar()
|
||||
const { decrementViewYear, incrementViewYear } = useCalendar()
|
||||
const { sortedMonths: months } = storeToRefs(useCalendar())
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
@@ -15,11 +15,11 @@ function handleWheel(e: WheelEvent) {
|
||||
}
|
||||
|
||||
const moveCalendarLeft = useThrottleFn(() => {
|
||||
decrementYear()
|
||||
decrementViewYear()
|
||||
}, 100)
|
||||
|
||||
const moveCalendarRight = useThrottleFn(() => {
|
||||
incrementYear()
|
||||
incrementViewYear()
|
||||
}, 100)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarMonth } from '~/models/CalendarMonth';
|
||||
import type { CalendarMonth } from "~/models/CalendarMonth";
|
||||
|
||||
defineProps<{
|
||||
month: CalendarMonth
|
||||
@@ -11,7 +11,7 @@ defineProps<{
|
||||
<div class="font-medium">
|
||||
{{ month.name }}
|
||||
</div>
|
||||
<div class="grid grid-cols-7">
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<CalendarStateYearlyDayTile
|
||||
v-for="day in month.days"
|
||||
:key="day"
|
||||
|
||||
21
components/global/AddCard.vue
Normal file
21
components/global/AddCard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhPlusCircle } from "@phosphor-icons/vue";
|
||||
|
||||
const emit = defineEmits(["on-click"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCard
|
||||
class="h-full lg:w-fit transition-all bg-transparent hover:text-slate-900 text-slate-500 dark:hover:text-slate-100 dark:text-slate-500 dark:focus-within:outline-gray-900 hover:-translate-y-0"
|
||||
has-click
|
||||
@on-click="emit('on-click')"
|
||||
>
|
||||
<UiCardHeader class="h-full justify-center items-center text-center gap-0">
|
||||
<PhPlusCircle size="38" weight="fill" />
|
||||
|
||||
<UiCardTitle class="text-xl">
|
||||
<slot />
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
</UiCard>
|
||||
</template>
|
||||
60
components/global/Breadcrumb.vue
Normal file
60
components/global/Breadcrumb.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { PhCaretRight } from "@phosphor-icons/vue";
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
export type BreadcrumbItem = {
|
||||
label?: string;
|
||||
translateKey?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
items?: BreadcrumbItem[];
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiBreadcrumb>
|
||||
<UiBreadcrumbList>
|
||||
<UiBreadcrumbItem v-if="route.path !== '/my'">
|
||||
<UiBreadcrumbLink as-child>
|
||||
<NuxtLink to="/my">
|
||||
{{ $t("breadcrumbs.profile") }}
|
||||
</NuxtLink>
|
||||
</UiBreadcrumbLink>
|
||||
</UiBreadcrumbItem>
|
||||
<UiBreadcrumbItem v-else>
|
||||
<UiBreadcrumbPage>
|
||||
{{ $t("breadcrumbs.profile") }}
|
||||
</UiBreadcrumbPage>
|
||||
</UiBreadcrumbItem>
|
||||
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<UiBreadcrumbSeparator>
|
||||
<PhCaretRight />
|
||||
</UiBreadcrumbSeparator>
|
||||
<UiBreadcrumbItem>
|
||||
<UiBreadcrumbLink v-if="item.to" as-child>
|
||||
<NuxtLink :to="item.to">
|
||||
<template v-if="item.label">
|
||||
{{ item.label }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t(`breadcrumbs.${item.translateKey}`) }}
|
||||
</template>
|
||||
</NuxtLink>
|
||||
</UiBreadcrumbLink>
|
||||
<UiBreadcrumbPage v-else>
|
||||
<template v-if="item.label">
|
||||
{{ item.label }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t(`breadcrumbs.${item.translateKey}`) }}
|
||||
</template>
|
||||
</UiBreadcrumbPage>
|
||||
</UiBreadcrumbItem>
|
||||
</template>
|
||||
</UiBreadcrumbList>
|
||||
</UiBreadcrumb>
|
||||
</template>
|
||||
@@ -1,5 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
type HeadingLevel = "h0" | "h1" | "h2" | "h3"
|
||||
|
||||
interface HeadingProps {
|
||||
level?: HeadingLevel
|
||||
}
|
||||
|
||||
withDefaults(defineProps<HeadingProps>(), {
|
||||
level: "h2"
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-2xl font-bold flex">
|
||||
<h1 v-if="level === 'h0'" class="text-4xl md:text-6xl font-bold flex">
|
||||
<slot />
|
||||
</h1>
|
||||
<h1 v-else-if="level === 'h1'" class="text-2xl md:text-4xl font-bold flex">
|
||||
<slot />
|
||||
</h1>
|
||||
<h2 v-else-if="level === 'h2'" class="text-xl md:text-2xl font-bold flex">
|
||||
<slot />
|
||||
</h2>
|
||||
<h3 v-if="level === 'h3'" class="text-lg md:text-xl font-bold flex">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
21
components/global/LoadingCard.vue
Normal file
21
components/global/LoadingCard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
const { lineNb = 3 } = defineProps<{
|
||||
lineNb?: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCard class="w-full h-full flex flex-col">
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>
|
||||
<UiSkeleton class="w-2/3 max-w-full h-8" />
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="grow grid gap-2">
|
||||
<UiSkeleton class="w-full max-w-full h-6" />
|
||||
<template v-for="(l, i) in lineNb" :key="i">
|
||||
<UiSkeleton class="max-w-full h-6" :style="`width: ${Math.floor(Math.random() * 100) + 1}%`" />
|
||||
</template>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</template>
|
||||
@@ -1,77 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhHouse, PhList } from '@phosphor-icons/vue'
|
||||
import type { SidebarProps } from './SidebarProps';
|
||||
|
||||
defineProps<SidebarProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid grid-rows-[1fr_auto] justify-center">
|
||||
<menu class="flex flex-col gap-4">
|
||||
<li class="mb-12">
|
||||
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||
<PhList size="27" />
|
||||
</UiButton>
|
||||
</li>
|
||||
|
||||
<li v-if="!isHome">
|
||||
<UiTooltipProvider :delay-duration="100">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
|
||||
<RouterLink to="/">
|
||||
<PhHouse size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'">
|
||||
<p>Retourner aux outils</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</li>
|
||||
|
||||
<li v-for="(item, i) in menuItems" :key="i">
|
||||
<UiTooltipProvider :delay-duration="100">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton v-if="item.to" variant="ghost" size="icon" class="rounded-full" as-child>
|
||||
<RouterLink :to="item.to">
|
||||
<component :is="item.phIcon" size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
<UiButton v-if="item.clickHandler" variant="ghost" size="icon" class="rounded-full" @click="item.clickHandler">
|
||||
<component :is="item.phIcon" size="24" weight="fill" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'">
|
||||
<p>{{ item.tooltip }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</li>
|
||||
|
||||
<!-- <li>
|
||||
<UiTooltipProvider :delay-duration="100">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-full"
|
||||
@click="revealAdvancedSearch()"
|
||||
>
|
||||
<PhMagnifyingGlass size="24" weight="fill" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'">
|
||||
<p>Recherche avancée</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</li> -->
|
||||
</menu>
|
||||
|
||||
<UserCTA />
|
||||
</nav>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
export interface MenuItem {
|
||||
phIcon: Component
|
||||
tooltip: string
|
||||
clickHandler?: () => void
|
||||
to?: string
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
menuItems: MenuItem[],
|
||||
isHome?: boolean
|
||||
}
|
||||
30
components/global/Spacing.vue
Normal file
30
components/global/Spacing.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface SpacingProps {
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xlg" | "2xl" | "3xl"
|
||||
}
|
||||
|
||||
const props = defineProps<SpacingProps>()
|
||||
|
||||
let spacingClass: string
|
||||
|
||||
switch (props.size) {
|
||||
case "lg":
|
||||
spacingClass = "space-y-4"
|
||||
break;
|
||||
|
||||
case "md":
|
||||
default:
|
||||
spacingClass = "space-y-2"
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(spacingClass)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
41
components/global/input/Color.vue
Normal file
41
components/global/input/Color.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type RPGColor, rpgColors } from "~/models/Color";
|
||||
|
||||
const { id, theme = "normal", position = "popper" } = defineProps<{
|
||||
id: string
|
||||
theme?: "normal" | "subtle"
|
||||
position?: "item-aligned" | "popper" | undefined
|
||||
}>();
|
||||
|
||||
const model = defineModel<RPGColor>({ default: "white" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSelect v-model="model">
|
||||
<UiSelectTrigger :id :class="cn({ 'h-9': theme === 'subtle' })">
|
||||
<UiSelectValue
|
||||
:placeholder="$t('ui.colors.selectOne')"
|
||||
class="bgc"
|
||||
:class="cn(`element-${model}`)"
|
||||
/>
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent :position>
|
||||
<UiSelectGroup>
|
||||
<UiSelectItem
|
||||
v-for="color in rpgColors"
|
||||
:key="color"
|
||||
:value="color"
|
||||
class="bgc"
|
||||
:class="
|
||||
cn(
|
||||
`element-${color}`,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ $t(`ui.colors.${color}`) }}
|
||||
</UiSelectItem>
|
||||
</UiSelectGroup>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</template>
|
||||
30
components/global/input/ContentState.vue
Normal file
30
components/global/input/ContentState.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { contentStates, type ContentState } from "~/models/Entity";
|
||||
|
||||
defineProps<{
|
||||
id: string
|
||||
}>();
|
||||
|
||||
const model = defineModel<ContentState>({ default: "draft" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSelect v-model="model">
|
||||
<UiSelectTrigger :id>
|
||||
<UiSelectValue
|
||||
:placeholder="$t('ui.contentState.selectOne')"
|
||||
/>
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent position="popper">
|
||||
<UiSelectGroup>
|
||||
<UiSelectItem
|
||||
v-for="state in contentStates"
|
||||
:key="state"
|
||||
:value="state"
|
||||
>
|
||||
{{ $t(`ui.contentState.${state}`) }}
|
||||
</UiSelectItem>
|
||||
</UiSelectGroup>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</template>
|
||||
114
components/global/sidebar/Sidebar.vue
Normal file
114
components/global/sidebar/Sidebar.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhList } from "@phosphor-icons/vue"
|
||||
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
const { currentMenu } = storeToRefs(useUiStore())
|
||||
|
||||
function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
||||
if (actionType === "event-search") {
|
||||
revealAdvancedSearch()
|
||||
}
|
||||
}
|
||||
|
||||
function computeMenuItemIcon(iconString: SidebarMenuIcon) {
|
||||
switch (iconString) {
|
||||
case "universe":
|
||||
return PhHurricane
|
||||
case "world":
|
||||
return PhGlobeHemisphereEast
|
||||
default:
|
||||
return PhCompass
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="w-16 py-6 border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 grid grid-rows-[1fr_auto] justify-center transition-colors after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60">
|
||||
<menu class="flex flex-col gap-4">
|
||||
<li class="mb-12">
|
||||
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||
<PhList size="27" />
|
||||
</UiButton>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<UiTooltipProvider :delay-duration="50">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
|
||||
<RouterLink to="/explore">
|
||||
<PhCompass size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
||||
<p>
|
||||
{{ $t('pages.explore.menuLabel') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</li>
|
||||
|
||||
<ClientOnly>
|
||||
<li v-for="(item, i) in currentMenu" :key="i">
|
||||
<UiTooltipProvider :delay-duration="50">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
v-if="item.to"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-full"
|
||||
as-child
|
||||
>
|
||||
<RouterLink :to="item.to">
|
||||
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
<UiButton
|
||||
v-if="item.action"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-full"
|
||||
@click="handleMenuItemAction(item.action!)"
|
||||
>
|
||||
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
||||
<p>{{ item.tooltip }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</li>
|
||||
</ClientOnly>
|
||||
</menu>
|
||||
|
||||
<UserCTA />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
nav {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
height: 25rem;
|
||||
max-height: 100%;
|
||||
background-image: url("/images/sidebar.png");
|
||||
background-position: bottom;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
mask-image: linear-gradient(to top, black 25%, transparent 50%, transparent);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
17
components/global/sidebar/SidebarProps.ts
Normal file
17
components/global/sidebar/SidebarProps.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type SidebarMenuActionType = "event-search"
|
||||
|
||||
export type SidebarMenuIcon = "universe" | "world"
|
||||
|
||||
export interface SidebarMenuItem {
|
||||
phIcon: SidebarMenuIcon,
|
||||
phIconWeight?: "regular" | "light" | "fill" | "duotone" | "bold" | "thin"
|
||||
highlight?: boolean
|
||||
tooltip: string
|
||||
action?: SidebarMenuActionType
|
||||
to?: string
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
menuItems: SidebarMenuItem[],
|
||||
isHome?: boolean
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed } from "vue"
|
||||
|
||||
import { PhUserCircle } from '@phosphor-icons/vue'
|
||||
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { auth } = useSupabaseClient()
|
||||
const user = useSupabaseUser()
|
||||
const userMeta = computed(() => user.value?.user_metadata)
|
||||
const profileUrl: string = `${useRequestURL().origin}/my/`
|
||||
|
||||
const { locale, setLocale } = useI18n()
|
||||
|
||||
const menuOpened = ref<boolean>(false)
|
||||
|
||||
@@ -19,12 +23,13 @@ watch(user, closeMenu)
|
||||
async function handleGoogleLogin() {
|
||||
try {
|
||||
auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
provider: "google",
|
||||
options: {
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent'
|
||||
}
|
||||
access_type: "offline",
|
||||
prompt: "consent"
|
||||
},
|
||||
redirectTo: profileUrl
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
@@ -42,40 +47,166 @@ async function handleLogout() {
|
||||
}
|
||||
}
|
||||
|
||||
function gotoProfilePage() {
|
||||
router.push({ path: '/profile' })
|
||||
type AvailableRoutes = "/my" | "/my/settings"
|
||||
|
||||
function pushRoute(to: AvailableRoutes) {
|
||||
router.push({ path: to })
|
||||
|
||||
closeMenu()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<UiPopover v-model:open="menuOpened">
|
||||
<UiPopoverTrigger>
|
||||
<UiAvatar v-if="user" class="cursor-pointer">
|
||||
<UiDropdownMenu v-model:open="menuOpened">
|
||||
<UiDropdownMenuTrigger>
|
||||
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer">
|
||||
<UiAvatarImage
|
||||
:src="userMeta?.avatar_url"
|
||||
:alt="userMeta?.full_name"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<UiAvatarFallback>Me</UiAvatarFallback>
|
||||
<UiAvatarFallback>
|
||||
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<UiButton v-else variant="outline" size="icon">
|
||||
<PhUserCircle size="18" />
|
||||
<UiButton v-else variant="outline" size="icon" class="rounded-full border-indigo-200 bg-indigo-700 dark:border-slate-300 dark:bg-neutral-950 dark:hover:bg-slate-50 dark:hover:text-slate-950 cursor-pointer">
|
||||
<PhUserCircle size="24" />
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-fit p-0" :align="'start'" :side="'top'" :collision-padding="20">
|
||||
<UiCommand>
|
||||
<UiCommandList v-if="user">
|
||||
<UiCommandGroup :heading="`Connecté en tant que ${user?.email}`">
|
||||
<UiCommandItem value="profile" @select="gotoProfilePage"> Profil </UiCommandItem>
|
||||
<UiCommandItem value="logout" @select="handleLogout"> Déconnexion </UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
<UiCommandList v-else>
|
||||
<UiCommandItem value="logout" @select="handleGoogleLogin"> Connexion </UiCommandItem>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</UiDropdownMenuTrigger>
|
||||
|
||||
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
|
||||
<template v-if="user">
|
||||
<p class="p-2 text-[.7em] opacity-75">
|
||||
{{ $t('ui.greeting', { user: user?.email }) }}
|
||||
</p>
|
||||
|
||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
|
||||
<PhUser size="20" weight="fill" />
|
||||
<span>
|
||||
{{ $t('ui.sidebarMenu.profile') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p class="p-2 text-[.7em] opacity-75">
|
||||
{{ $t('ui.anonymousGreeting') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<UiDropdownMenuSub>
|
||||
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
|
||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
|
||||
<PhPalette size="20" weight="fill" />
|
||||
<span>
|
||||
{{ $t('ui.sidebarMenu.appearance') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuSubTrigger>
|
||||
<UiDropdownMenuPortal>
|
||||
<UiDropdownMenuSubContent>
|
||||
<UiDropdownMenuItem
|
||||
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||
:class="cn({ 'text-emerald-600': $colorMode.preference === 'dark' })"
|
||||
@select.prevent="$colorMode.preference = 'dark'"
|
||||
>
|
||||
<PhCheckCircle v-if="$colorMode.preference === 'dark'" size="20" weight="fill" />
|
||||
<PhMoon v-else size="20" />
|
||||
|
||||
<span>
|
||||
{{ $t('ui.dark') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem
|
||||
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||
:class="cn({ 'text-emerald-600': $colorMode.preference === 'light' })"
|
||||
@select.prevent="$colorMode.preference = 'light'"
|
||||
>
|
||||
<PhCheckCircle v-if="$colorMode.preference === 'light'" size="20" weight="fill" />
|
||||
<PhSun v-else size="20" />
|
||||
|
||||
<span>
|
||||
{{ $t('ui.light') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem
|
||||
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||
:class="cn({ 'text-emerald-600': $colorMode.preference === 'system' })"
|
||||
@select.prevent="$colorMode.preference = 'system'"
|
||||
>
|
||||
<PhCheckCircle v-if="$colorMode.preference === 'system'" size="20" weight="fill" />
|
||||
<PhLaptop v-else size="20" />
|
||||
|
||||
<span>
|
||||
{{ $t('ui.system') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuSubContent>
|
||||
</UiDropdownMenuPortal>
|
||||
</UiDropdownMenuSub>
|
||||
|
||||
<UiDropdownMenuSub>
|
||||
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
|
||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
|
||||
<PhTranslate size="20" />
|
||||
<span>
|
||||
{{ $t('ui.sidebarMenu.language') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuSubTrigger>
|
||||
<UiDropdownMenuPortal>
|
||||
<UiDropdownMenuSubContent>
|
||||
<UiDropdownMenuSubContent>
|
||||
<UiDropdownMenuItem
|
||||
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||
:class="cn({ 'text-emerald-600': locale === 'fr' })"
|
||||
@select.prevent="setLocale('fr')"
|
||||
>
|
||||
<PhCheckCircle v-if="locale === 'fr'" size="20" weight="fill" />
|
||||
<img v-else src="/images/flag-fr.png" width="20" alt="" loading="eager">
|
||||
<span>Français</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem
|
||||
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||
:class="cn({ 'text-emerald-600': locale === 'en' })"
|
||||
@select.prevent="setLocale('en')"
|
||||
>
|
||||
<PhCheckCircle v-if="locale === 'en'" size="20" weight="fill" />
|
||||
<img v-else src="/images/flag-uk.png" width="20" alt="" loading="eager">
|
||||
<span>English</span>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuSubContent>
|
||||
</UiDropdownMenuSubContent>
|
||||
</UiDropdownMenuPortal>
|
||||
</UiDropdownMenuSub>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
|
||||
<template v-if="user">
|
||||
<!-- <UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my/settings')">
|
||||
<PhGear size="20" weight="fill" />
|
||||
<span>
|
||||
{{ $t('ui.sidebarMenu.account') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem> -->
|
||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="handleLogout">
|
||||
<PhSignOut size="20" weight="fill" />
|
||||
<span>
|
||||
{{ $t('ui.sidebarMenu.logout') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<UiDropdownMenuItem v-if="!user" class="flex gap-[.5ch] items-center rounded-none" @click="handleGoogleLogin">
|
||||
<PhSignIn size="18" weight="fill" />
|
||||
<span>
|
||||
{{ $t('ui.sidebarMenu.login') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const { data: worlds } = await useLazyFetch('/api/worlds')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="p-8">
|
||||
<Heading>{{ user?.user_metadata.full_name }}</Heading>
|
||||
|
||||
<section v-if="worlds?.data" class="mt-4">
|
||||
<h2 class="mb-4 text-lg font-bold">
|
||||
Mondes
|
||||
</h2>
|
||||
|
||||
<ul class="grid md:grid-cols-3 gap-2">
|
||||
<li v-for="(world, i) in worlds.data" :key="i">
|
||||
<UiCard
|
||||
class="w-full transition-all"
|
||||
:link="`/calendar/${world.id}`"
|
||||
:class="{
|
||||
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
|
||||
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': world.color === 'red',
|
||||
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': world.color === 'orange',
|
||||
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': world.color === 'amber',
|
||||
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': world.color === 'yellow',
|
||||
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': world.color === 'lime',
|
||||
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': world.color === 'green',
|
||||
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': world.color === 'emerald',
|
||||
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': world.color === 'teal',
|
||||
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': world.color === 'cyan',
|
||||
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': world.color === 'sky',
|
||||
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': world.color === 'blue',
|
||||
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': world.color === 'indigo',
|
||||
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': world.color === 'violet',
|
||||
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': world.color === 'purple',
|
||||
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': world.color === 'fuchsia',
|
||||
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
|
||||
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
|
||||
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
|
||||
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white',
|
||||
}"
|
||||
>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ world.name }}</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<p class="italic">{{ world.description }}</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
14
components/ui/alert-dialog/AlertDialog.vue
Normal file
14
components/ui/alert-dialog/AlertDialog.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { type AlertDialogEmits, type AlertDialogProps, AlertDialogRoot, useForwardPropsEmits } from "radix-vue"
|
||||
|
||||
const props = defineProps<AlertDialogProps>()
|
||||
const emits = defineEmits<AlertDialogEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
20
components/ui/alert-dialog/AlertDialogAction.vue
Normal file
20
components/ui/alert-dialog/AlertDialogAction.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue"
|
||||
import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button/index"
|
||||
|
||||
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
|
||||
<slot />
|
||||
</AlertDialogAction>
|
||||
</template>
|
||||
20
components/ui/alert-dialog/AlertDialogCancel.vue
Normal file
20
components/ui/alert-dialog/AlertDialogCancel.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue"
|
||||
import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button/index"
|
||||
|
||||
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogCancel v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)">
|
||||
<slot />
|
||||
</AlertDialogCancel>
|
||||
</template>
|
||||
0
components/ui/alert-dialog/AlertDialogClose.vue
Normal file
0
components/ui/alert-dialog/AlertDialogClose.vue
Normal file
42
components/ui/alert-dialog/AlertDialogContent.vue
Normal file
42
components/ui/alert-dialog/AlertDialogContent.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue"
|
||||
import {
|
||||
AlertDialogContent,
|
||||
type AlertDialogContentEmits,
|
||||
type AlertDialogContentProps,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<AlertDialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay
|
||||
class="fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<AlertDialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</template>
|
||||
25
components/ui/alert-dialog/AlertDialogDescription.vue
Normal file
25
components/ui/alert-dialog/AlertDialogDescription.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue"
|
||||
import {
|
||||
AlertDialogDescription,
|
||||
type AlertDialogDescriptionProps,
|
||||
} from "radix-vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogDescription
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogDescription>
|
||||
</template>
|
||||
21
components/ui/alert-dialog/AlertDialogFooter.vue
Normal file
21
components/ui/alert-dialog/AlertDialogFooter.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
16
components/ui/alert-dialog/AlertDialogHeader.vue
Normal file
16
components/ui/alert-dialog/AlertDialogHeader.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
22
components/ui/alert-dialog/AlertDialogTitle.vue
Normal file
22
components/ui/alert-dialog/AlertDialogTitle.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue"
|
||||
import { AlertDialogTitle, type AlertDialogTitleProps } from "radix-vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTitle
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-lg font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogTitle>
|
||||
</template>
|
||||
11
components/ui/alert-dialog/AlertDialogTrigger.vue
Normal file
11
components/ui/alert-dialog/AlertDialogTrigger.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "radix-vue"
|
||||
|
||||
const props = defineProps<AlertDialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</AlertDialogTrigger>
|
||||
</template>
|
||||
9
components/ui/alert-dialog/index.ts
Normal file
9
components/ui/alert-dialog/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as AlertDialog } from "./AlertDialog.vue"
|
||||
export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue"
|
||||
export { default as AlertDialogContent } from "./AlertDialogContent.vue"
|
||||
export { default as AlertDialogHeader } from "./AlertDialogHeader.vue"
|
||||
export { default as AlertDialogTitle } from "./AlertDialogTitle.vue"
|
||||
export { default as AlertDialogDescription } from "./AlertDialogDescription.vue"
|
||||
export { default as AlertDialogFooter } from "./AlertDialogFooter.vue"
|
||||
export { default as AlertDialogAction } from "./AlertDialogAction.vue"
|
||||
export { default as AlertDialogCancel } from "./AlertDialogCancel.vue"
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { AvatarRoot } from 'radix-vue'
|
||||
import { type AvatarVariants, avatarVariant } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { AvatarRoot } from "radix-vue"
|
||||
import { type AvatarVariants, avatarVariant } from "."
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
size?: AvatarVariants['size']
|
||||
shape?: AvatarVariants['shape']
|
||||
class?: HTMLAttributes["class"]
|
||||
size?: AvatarVariants["size"]
|
||||
shape?: AvatarVariants["shape"]
|
||||
}>(), {
|
||||
size: 'sm',
|
||||
shape: 'circle',
|
||||
size: "sm",
|
||||
shape: "circle",
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
|
||||
import { AvatarFallback, type AvatarFallbackProps } from "radix-vue"
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
|
||||
import { AvatarImage, type AvatarImageProps } from "radix-vue"
|
||||
|
||||
const props = defineProps<AvatarImageProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
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 AvatarImage } from './AvatarImage.vue'
|
||||
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||
export { default as Avatar } from "./Avatar.vue"
|
||||
export { default as AvatarImage } from "./AvatarImage.vue"
|
||||
export { default as AvatarFallback } from "./AvatarFallback.vue"
|
||||
|
||||
export const avatarVariant = cva(
|
||||
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
||||
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-10 w-10 text-xs',
|
||||
base: 'h-16 w-16 text-2xl',
|
||||
lg: 'h-32 w-32 text-5xl',
|
||||
sm: "h-10 w-10 text-xs",
|
||||
base: "h-16 w-16 text-2xl",
|
||||
lg: "h-32 w-32 text-5xl",
|
||||
},
|
||||
shape: {
|
||||
circle: 'rounded-full',
|
||||
square: 'rounded-md',
|
||||
circle: "rounded-full",
|
||||
square: "rounded-md",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { type BadgeVariants, badgeVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { type BadgeVariants, badgeVariants } from "."
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
variant?: BadgeVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: BadgeVariants["variant"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
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 "./Badge.vue"
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-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: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
13
components/ui/breadcrumb/Breadcrumb.vue
Normal file
13
components/ui/breadcrumb/Breadcrumb.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav aria-label="breadcrumb" :class="props.class">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
22
components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
22
components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { MoreHorizontal } from "lucide-vue-next"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="h-4 w-4" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
</template>
|
||||
16
components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
16
components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
:class="cn('inline-flex items-center gap-1.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
20
components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
20
components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PrimitiveProps } from "radix-vue"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Primitive } from "radix-vue"
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
as: "a",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('transition-colors hover:text-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
16
components/ui/breadcrumb/BreadcrumbList.vue
Normal file
16
components/ui/breadcrumb/BreadcrumbList.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
19
components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
19
components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
:class="cn('font-normal text-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
21
components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
21
components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronRight } from "lucide-vue-next"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('[&>svg]:w-3 [&>svg]:h-3', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronRight />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
7
components/ui/breadcrumb/index.ts
Normal file
7
components/ui/breadcrumb/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as Breadcrumb } from "./Breadcrumb.vue"
|
||||
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue"
|
||||
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue"
|
||||
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue"
|
||||
export { default as BreadcrumbList } from "./BreadcrumbList.vue"
|
||||
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue"
|
||||
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue"
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive, type PrimitiveProps } from 'radix-vue'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { Primitive, type PrimitiveProps } from "radix-vue"
|
||||
import { type ButtonVariants, buttonVariants } from "."
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: ButtonVariants["variant"]
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
searchSlash?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button'
|
||||
as: "button"
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -20,15 +20,15 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class, 'group')"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class, 'group cursor-pointer')"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<span
|
||||
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"
|
||||
>
|
||||
<span class="-mt-[2px]">CTRL + :</span>
|
||||
<kbd class="-mt-[2px]">CTRL + :</kbd>
|
||||
</span>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
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 "./Button.vue"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
"inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/20",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
xs: 'h-7 rounded-sm px-2 text-xs',
|
||||
sm: 'h-9 rounded-md px-3 text-sm',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10'
|
||||
default: "h-10 px-4 py-2",
|
||||
xs: "h-7 rounded-sm px-2 text-xs",
|
||||
sm: "h-9 rounded-md px-3 text-sm",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
class?: HTMLAttributes["class"]
|
||||
link?: string
|
||||
hasClick?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["on-click"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn('rounded-lg border bg-card text-card-foreground shadow-sm transition-all', props.class, {
|
||||
cn('rounded-lg border bg-card text-card-foreground shadow-xs transition-all isolate', props.class, {
|
||||
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
|
||||
props.link
|
||||
props.link || props.hasClick
|
||||
})
|
||||
"
|
||||
>
|
||||
@@ -22,7 +25,11 @@ const props = defineProps<{
|
||||
<NuxtLink
|
||||
v-if="props.link"
|
||||
:to="props.link"
|
||||
class="absolute inset-0 focus-visible:outline-none"
|
||||
class="absolute inset-0 z-10 focus-visible:outline-hidden"
|
||||
/>
|
||||
<button
|
||||
v-if="props.hasClick"
|
||||
class="absolute inset-0 z-10 focus-visible:outline-hidden"
|
||||
@click="emit('on-click')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user