176 Commits

Author SHA1 Message Date
Alexis
8eecad6488 Fixed calendar event issues ? 2025-02-27 22:25:28 +01:00
Alexis
8ff5d5941a Updated major versions 2025-02-14 16:55:47 +01:00
Alexis
fe9a58b0ea Updated minor versions 2025-02-14 16:44:45 +01:00
Alexis
dcb637021e Hotfix : Added RLS for world 2024-12-20 17:44:54 +01:00
AlexisNP
5689d2689d Merge pull request #60 from AlexisNP/bugfix/quick-create-event-modal
Bugfix/quick create event modal
2024-12-20 11:49:14 +01:00
Alexis
91048d1b8e Watched for user change in the page instead 2024-12-20 11:41:17 +01:00
Alexis
3db1654682 Removed console log 2024-12-20 11:31:40 +01:00
Alexis
9f6d318a15 Removed duplicate closing calls for create event popover 2024-12-20 11:29:13 +01:00
Alexis
eedbf1decb Removed user watcher for now 2024-12-20 11:16:41 +01:00
Alexis
bc6cbd839f Added event count for calendars 2024-12-19 20:18:41 +01:00
Alexis
bf8257e60f Added explorer page 2024-12-19 19:38:57 +01:00
Alexis
199cfb6634 Added not found page for calendars 2024-12-19 16:46:22 +01:00
Alexis
df8af541c1 Added updated toast 2024-12-19 16:28:38 +01:00
Alexis
dde3f8504a Fixed updating modal not closing as well 2024-12-19 16:16:45 +01:00
AlexisNP
01fe9f3281 Merge pull request #59 from AlexisNP/bugfix/modal-issues 2024-12-19 15:55:52 +01:00
Alexis
bc7a400826 Fixed quick create event modal not closing on submit
This was caused by a missing event handler
2024-12-19 14:03:20 +01:00
Alexis
c99059d9a8 Fixed event popover not closing on submit
This was caused by a random stray isLoading
2024-12-19 14:01:05 +01:00
Alexis
cd965f61e8 Fixed event creation popover appearing for anon users 2024-12-19 13:52:15 +01:00
AlexisNP
9d5ff6a0b8 Merge pull request #57 from AlexisNP/features/calendar-public-links
Fixed calendar having null as visibility
2024-12-19 09:21:07 +01:00
Alexis
06fccd83ca Fixed calendar having null as visibility 2024-12-19 09:13:42 +01:00
AlexisNP
5f8ae1fb6c Merge pull request #56 from AlexisNP/features/calendar-public-links
Features/calendar public links
2024-12-18 20:17:43 +01:00
Alexis
535a73162c Added user change validation and read status
This might be better to just refetch the data with anonymous authorizations
2024-12-17 17:17:13 +01:00
Alexis
49574d8ef1 Added first draft of story 2024-12-09 23:58:16 +01:00
AlexisNP
323eedec56 Merge pull request #54 from AlexisNP/features/add-create-event-button
Features/add create event button
2024-12-03 15:31:54 +01:00
Alexis
4ddbfd9855 Changed organization for 2 mixed components 2024-12-03 15:29:06 +01:00
Alexis
bb6df1d440 Fixed organization 2024-12-03 15:21:36 +01:00
Alexis
dd1cb1f420 Add quick create event button 2024-12-03 14:53:24 +01:00
AlexisNP
11252c168e Merge pull request #50 from AlexisNP/bugfix/fix-leftover-hardcoded-logic
Bugfix/fix leftover hardcoded logic
2024-12-02 18:34:55 +01:00
Alexis
acd0476970 Removed paged component nav
As this will be moved to their own files and on the server side, I see little reason to refactor them, just so they'll be refactored again down the line.
2024-12-02 18:31:08 +01:00
Alexis
24c048df4d Fixed monthsPerYear leftover data 2024-11-27 19:36:37 +01:00
AlexisNP
c55d690f8b Merge pull request #49 from AlexisNP/bugfix/event-details-loses-focus-twice
Added prevent on focus outside event for event details
2024-11-27 17:18:11 +01:00
Alexis
39e4ac33b7 Added prevent on focus outside event for event details 2024-11-27 17:16:10 +01:00
AlexisNP
568efa2251 Merge pull request #48 from AlexisNP/features/nav-should-toast
Added toast when navigating out of bounds of event pages
2024-11-27 14:10:57 +01:00
Alexis
6a68105a0f Added toast when navigating out of bounds of event pages 2024-11-27 14:07:18 +01:00
AlexisNP
bba01fdcf1 Merge pull request #47 from AlexisNP/bugfix/one-event-fills-the-stack
Removed overkill pattern
2024-11-26 20:03:30 +01:00
Alexis
76a6468a04 Removed overkill patterns
This fixes the stack being trashed up
2024-11-26 19:59:18 +01:00
Alexis
9fa6af2fa3 Merge branch 'dev' 2024-11-26 13:42:43 +01:00
Alexis
1f15a66505 Added nuxthub 2024-11-26 13:42:22 +01:00
AlexisNP
8562ea6362 Merge pull request #45 from AlexisNP/features/add-limit-to-event-text-fields
Add limit to event text fields
2024-11-25 18:17:04 +01:00
Alexis
378f07a862 User locale now saves ! 2024-11-25 18:09:18 +01:00
Alexis
d774fde1dd Added client side validation for fields 2024-11-25 17:41:40 +01:00
Alexis
863937bb16 Changed limit and added scrollbars on details 2024-11-25 16:33:02 +01:00
Alexis
372c7a49e1 Added validation for location as well 2024-11-24 20:08:30 +01:00
Alexis
c7069164b0 Added check on client side (api) 2024-11-24 19:56:00 +01:00
Alexis
5258ed3f2a Added server limit to event title and description 2024-11-24 15:30:50 +01:00
AlexisNP
66e6f84f8b Merge pull request #44 from AlexisNP/bugfix/empty-search-categories
Fixed empty categories
2024-11-22 17:54:37 +01:00
Alexis
190a94f96e Fixed category filter functions 2024-11-22 17:47:25 +01:00
Alexis
bbf1cbf84e Added API data for categories 2024-11-22 16:41:01 +01:00
AlexisNP
c58176a9e2 Merge pull request #42 from AlexisNP/bugfix/today-button-issues
Today menu is disabled only if current screen is not the default one
2024-11-19 17:26:58 +01:00
Alexis
e5fb40c24c Today menu is disabled only if current screen is not the default one 2024-11-19 17:25:45 +01:00
AlexisNP
bf36be3aa1 Merge pull request #41 from AlexisNP/features/world-managment-functions 2024-09-09 23:48:09 +02:00
Alexis
636623ee96 Added world deletion actions 2024-09-09 23:21:27 +02:00
Alexis
c19262e3c2 Added world creation dialog 2024-09-09 22:09:59 +02:00
AlexisNP
f6898da2f2 Merge pull request #37 from AlexisNP/features/toast-service 2024-09-08 19:40:56 +02:00
Alexis
0f5bc219f2 Changed slightly user menu 2024-09-08 19:37:50 +02:00
Alexis
2f79de8483 Added toast on deleted entities 2024-09-08 19:26:38 +02:00
Alexis
a69553e089 Added toast from shadcn 2024-09-08 18:10:54 +02:00
AlexisNP
680e5fe35c Merge pull request #36 from AlexisNP/features/i18n 2024-09-08 18:00:23 +02:00
Alexis
36bffbd0ab Fixed nuxt i18n cookie config 2024-09-08 17:47:58 +02:00
Alexis
63c1cef77e Added last translations for event forms / data 2024-09-08 15:30:10 +02:00
Alexis
11d900e7f7 Added compact translations for date duration 2024-09-05 21:16:24 +02:00
Alexis
f42a68dfc9 Added date computing translations 2024-09-05 21:01:26 +02:00
Alexis
74427a5056 Added relative days translations 2024-09-05 17:59:44 +02:00
Alexis
872e1be949 Updated translations for calendar time nav 2024-09-05 17:54:46 +02:00
Alexis
49cb74df7d Added extra translations to display mode 2024-09-05 17:46:32 +02:00
Alexis
6cfcde3b58 Added a bunch of translations on calendar components 2024-09-03 22:19:07 +02:00
Alexis
6fee4c7158 Added translations for more calendar forms 2024-09-03 18:23:28 +02:00
Alexis
85c661eda5 Added more translations to meta and delete modals 2024-09-02 20:07:01 +02:00
Alexis
6d66b599cf Added sidebar translations 2024-09-02 18:34:24 +02:00
Alexis
edb939bb39 Added base i18n support 2024-09-02 18:10:34 +02:00
AlexisNP
fc91e4825a Merge pull request #33 from AlexisNP/features/light-theme
Add the light theme features
2024-09-01 21:21:20 +02:00
Alexis
7f2b5e2811 Fixed light theme for yearly layout 2024-09-01 21:19:54 +02:00
Alexis
9863ac9778 Fixed badge and callout colours 2024-09-01 13:58:00 +02:00
Alexis
c1b60942d2 Fixed month list overflow when adding a calendar 2024-09-01 12:26:17 +02:00
Alexis
44b0339874 Added light friendly event details 2024-09-01 12:16:36 +02:00
Alexis
6782b9bd41 Added light friendly calendar 2024-09-01 11:23:06 +02:00
Alexis
e71a810acd Changed accent colors 2024-08-29 18:26:40 +02:00
Alexis
67aefb8bd9 Revamped main layout 2024-08-29 17:41:58 +02:00
AlexisNP
386b714f3c Merge pull request #30 from AlexisNP/bugfix/27-advanced-search-filter 2024-08-26 23:06:42 +02:00
Alexis
cbc5bbeee0 Fixed search and pagination bug 2024-08-26 22:58:53 +02:00
Alexis
168a0b61d5 Hotfix : Removed log from template 2024-08-26 22:38:27 +02:00
AlexisNP
4bc1e77e76 Merge pull request #28 from AlexisNP/features/custom-calendar-creation
LGTM
2024-08-26 22:23:16 +02:00
Alexis
ebc715a3a6 Updated title functions on pages 2024-08-26 22:07:05 +02:00
Alexis
3f201b90e3 Refactored user menu and titles 2024-08-25 18:24:59 +02:00
Alexis
8b1eb9c258 Changed user menu / sidebar 2024-08-25 16:01:23 +02:00
Alexis
23a9f68b7b Changed profile from /i to /me 2024-08-25 15:24:36 +02:00
Alexis
05ae4bfb1f Redirect to profile after signin 2024-08-25 14:37:32 +02:00
Alexis
6c73578b7f Added subscriptions to world list 2024-08-25 14:18:11 +02:00
Alexis
5a47cb3563 Changed quote styles 2024-08-25 14:14:40 +02:00
Alexis
07e97873ef First draft of calendar deletion 2024-08-25 00:35:24 +02:00
Alexis
feb04912ca Merge branch 'features/custom-calendar-creation-refactor' into features/custom-calendar-creation 2024-08-24 22:35:03 +02:00
Alexis
1985ffdbe9 Added realtime subcription to calendar page 2024-08-24 22:34:51 +02:00
Alexis
245b46661a Added loading state to create calendar form 2024-08-24 21:38:15 +02:00
Alexis
99e478951d Added cancel action for dialog create 2024-08-24 21:24:08 +02:00
Alexis
3c8a018066 Added skeleton name display on dialog 2024-08-24 19:06:21 +02:00
Alexis
69b294dcf6 Fixed supabase redirect 2024-08-24 18:37:00 +02:00
Alexis
e3a6ff8790 Added basic caching for calendar page 2024-08-24 18:33:54 +02:00
Alexis
19a71d356e Fixed horrible loading pattern for calendar 2024-08-24 14:01:16 +02:00
Alexis
23e996ee2e Reverted upgrade changes 2024-08-24 10:50:30 +02:00
Alexis
7c81d633e5 Updated nuxt and dependencies 2024-08-23 20:05:36 +02:00
Alexis
a7cf648f2f Added correct RLS and month api routes 2024-08-23 16:34:47 +02:00
Alexis
db96e66b7a Changed loading and added spinner 2024-08-21 22:52:36 +02:00
Alexis
73ff951a10 Changed layout for hidden events somewhat 2024-08-21 18:45:24 +02:00
Alexis
9f4f30cebe Fixed issue with calendar today loading 2024-08-21 13:50:37 +02:00
Alexis
1137b172e4 Removed console log 2024-08-20 19:22:27 +02:00
Alexis
9062c4f946 Fixed events not appearing correctly 2024-08-20 18:34:28 +02:00
Alexis
d54e7eb9e9 Added categories back to calendar init 2024-08-20 17:57:55 +02:00
Alexis
fe98b770e1 Formatted some function names 2024-08-20 17:34:34 +02:00
Alexis
c84099fa54 Removed default date abstraction
This would cause problems after initial store load
2024-08-20 17:29:35 +02:00
Alexis
065b83debd Fixed calendar grid not loading during refresh 2024-08-20 17:18:17 +02:00
Alexis
06dd1d976d Fixed default date loading 2024-08-20 17:17:29 +02:00
Alexis
e978acfc71 Refactored the store system 2024-08-20 09:15:05 +02:00
Alexis
7244120219 Added day selection and revamped modal structure 2024-08-17 11:33:42 +02:00
Alexis
0d57c70719 Moved month list to sub component
This is better in the long run, but it also solves the problem with useSortable ; the composable required a rerender while it's in a tabs layer
2024-06-24 18:33:38 +02:00
Alexis
12bf5139da Added tabs to form 2024-06-24 18:01:39 +02:00
Alexis
008ec4fcef Changed views data 2024-06-14 21:48:39 +02:00
Alexis
328fc555ea Fixed loading of calendar 2024-06-13 20:55:50 +02:00
Alexis
bc29129546 Changed ways to access calendars 2024-06-13 19:10:03 +02:00
AlexisNP
a7cb032b12 Merge pull request #24 from AlexisNP/features/add-categories-management-to-event-cr
Add categories to event
2024-06-11 22:36:13 +02:00
Alexis
fe8ab76aaf Separated multi and single category component
I also disabled secondary categories because they shouldn't be used right now, it adds too much complexity
2024-06-11 21:38:47 +02:00
Alexis
23365d6324 Added filtering of categories 2024-06-11 17:51:50 +02:00
Alexis
72fa9a982b Changed spacing 2024-06-11 17:16:04 +02:00
Alexis
9d44b900b0 Reverted commit 15527fd983 2024-06-11 17:08:51 +02:00
Alexis
8c6a60aa40 Lowercase category tags 2024-06-10 22:50:21 +02:00
Alexis
90be6baf42 Fixed reactivity issue with bordered event CTA 2024-06-10 22:48:28 +02:00
Alexis
87a73e7f43 Fixed event details colors 2024-06-10 22:46:59 +02:00
Alexis
9127346131 Added standardized colors 2024-06-10 22:25:34 +02:00
Alexis
69f18e9338 Added category schema validation 2024-06-10 20:43:05 +02:00
Alexis
a1aa512018 Added collision detection for category list 2024-06-10 20:26:21 +02:00
Alexis
17b9937c6a Added category input component 2024-06-10 18:58:08 +02:00
AlexisNP
e9ec2cb3ae Merge pull request #23 from AlexisNP/features/add-hidden-event-functionnality
Add hidden events UI
2024-06-09 17:27:09 +02:00
Alexis
15527fd983 Changed create event trigger to be double click
This might get reverted, I'm testing to see how it feels first
2024-06-09 17:22:56 +02:00
Alexis
92850861cd Added private event visual feedback 2024-06-09 17:20:21 +02:00
Alexis
737ee6739b Added switch component to toggle visibility 2024-06-09 14:27:53 +02:00
Alexis
43768cb49d Added shadcn switch 2024-06-09 14:03:12 +02:00
AlexisNP
2330db31dc Merge pull request #22 from AlexisNP/features/add-quick-actions-for-events
Added quick actions for events
2024-06-09 12:43:30 +02:00
Alexis
f7c135f498 Added quick delete if the popover for one event is deployed 2024-06-09 12:37:01 +02:00
Alexis
f4ad06d744 Added back button for delete modal 2024-06-09 11:37:30 +02:00
Alexis
8d19f0aca2 Made delete button destructive 2024-06-09 10:54:07 +02:00
Alexis
017b54dec7 Added pencil icon to edit modal 2024-06-09 10:49:48 +02:00
Alexis
78c1387602 Added delete for quick removal 2024-06-09 10:44:45 +02:00
Alexis
8913a7d1b3 Added double click for quick edit 2024-06-09 10:35:53 +02:00
AlexisNP
746897cb6e Merge pull request #19 from AlexisNP/bugfix/event-operations-should-have-a-loading
Bugfix/event operations should have a loading
2024-06-08 22:31:46 +02:00
Alexis
d7ecb379fd Changed alert dialog for regular dialog in update event form 2024-06-08 22:15:56 +02:00
Alexis
d88ede5e21 Removed extraneous props 2024-06-08 22:13:21 +02:00
Alexis
6bb874f799 Fixed ref being used for component and not shallowRef 2024-06-08 22:06:23 +02:00
Alexis
6738ac76c4 Added loading and cancelling to deleting popover 2024-06-08 22:00:07 +02:00
Alexis
721e26d8e4 Added loading and cancelling to updating popover 2024-06-08 21:53:16 +02:00
Alexis
a07afc8351 Prevented other popovers from opening when an operation is underway 2024-06-08 18:08:08 +02:00
Alexis
a58c1eeab9 Added request cancel on create
This seems to not be always effective on really low connections, as the cancel request doesn't seem to be instantanneous.
2024-06-08 17:48:32 +02:00
Alexis
0c48350271 Disabled close interactions on create event popover 2024-06-08 16:45:36 +02:00
Alexis
c65bcc9ae9 Added loading indicator on single create event 2024-06-08 16:22:07 +02:00
AlexisNP
7959ad2345 Merge pull request #18 from AlexisNP/bugfix/make-the-sidebar-a-global-component
Sidebar is now a global component
2024-06-08 15:02:22 +02:00
Alexis
4c0a4031a1 Reinstated isHome check on sidebar 2024-06-08 15:01:15 +02:00
Alexis
9aa2d26135 Added break-words and hypens to prevent event cta from overflowing 2024-06-08 14:49:52 +02:00
Alexis
9314a8c3c2 Removed unfinished view types 2024-06-08 14:33:34 +02:00
Alexis
3002ae593c Fixed crash with function stringify 2024-06-08 14:18:19 +02:00
Alexis
f963116018 Closed user menu on select profile 2024-06-07 19:40:55 +02:00
Alexis
4a4433dcdd Moved Sidebar to top of app
This prevents flicker and stops useless rerender of the component
2024-06-07 19:35:49 +02:00
AlexisNP
ad79ca4186 Merge pull request #17 from AlexisNP/features/add-a-location-option-for-events
Location option for events
2024-06-07 16:50:03 +02:00
Alexis
6137ba3902 Removed required field for location 2024-06-07 16:48:52 +02:00
Alexis
e63422f091 Added location to database, model, and event templates 2024-06-06 22:43:31 +02:00
AlexisNP
c6009a4bca Merge pull request #14 from AlexisNP/features/increase-event-label-to-2-lines
Added line clamp to event CTA
2024-06-06 21:45:35 +02:00
Alexis
94fcd1c04e Added line clamp to event CTA 2024-06-06 21:43:32 +02:00
AlexisNP
0baf33ee19 Merge pull request #9 from AlexisNP/bugfix/add-alert-modal-for-event-deletion
Added alert modals for events
2024-06-05 23:34:07 +02:00
Alexis
85e6710560 Removed useless prop from alert dialog 2024-06-05 23:24:37 +02:00
Alexis
4972c26385 Removed resetSkeleton call
Same logic from commit b71f7745a4, as this would remove useful data from the skeleton used to splice / change the client side data
2024-06-05 23:21:33 +02:00
Alexis
05f5c5a92c Added actions / cancel to alert modals 2024-06-05 22:56:41 +02:00
Alexis
edeaaa1e19 Removed redundant data pass 2024-06-05 21:39:47 +02:00
Alexis
06b7dadf81 Added alert dialog
The shadcn cli doesn't seem to work with this one so I grabbed it from their github
2024-06-05 18:55:40 +02:00
Alexis
53024c6117 Added delete modal confirmation 2024-06-05 18:35:13 +02:00
Alexis
6749fdb46e Moved event modal states and functions to event store 2024-06-05 18:28:38 +02:00
AlexisNP
28cdffa90b Merge pull request #7 from AlexisNP/bugfix/prevent-event-stutter
Added rules for events' TransitionGroup
2024-06-05 18:06:21 +02:00
Alexis
476ca5f949 Added rules for events' TransitionGroup
This fixes the stutter that happens when an event is added / removed from the list of the current day tile
2024-06-05 18:05:14 +02:00
AlexisNP
170416a08f Merge pull request #5 from AlexisNP/bugfix/prevent-date-flash
Removed resetSkeleton on popover dismiss
2024-06-05 17:20:49 +02:00
Alexis
b71f7745a4 Removed resetSkeleton on popover dismiss
As the skeleton is already reset when it opens, it's redundant to reset it while it's not being displayed
2024-06-05 17:18:48 +02:00
228 changed files with 11907 additions and 6616 deletions

35
app.vue
View File

@@ -1,28 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { ConfigProvider } from 'radix-vue' import { ConfigProvider } from "radix-vue"
useHead({ useHead({
titleTemplate: (titleChunk) => { titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} — TTTools` : 'TTTools'; return titleChunk ? `${titleChunk} — TTTools` : "TTTools";
}, },
meta: [ meta: [
{ name: "charset", content: "UTF-8" }, { name: "charset", content: "UTF-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1.0" }, { name: "viewport", content: "width=device-width, initial-scale=1.0" },
{ name: "author", content: "Alexis Pelé" }, { 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: "generator", content: "Nuxt" },
{ name: "msapplication-TileColor", content: "00aba9" }, { name: "msapplication-TileColor", content: "00aba9" },
{ name: "theme-color", content: "00aba9" }, { name: "theme-color", content: "00aba9" },
{ name: "og:type", content: "tabletop-tools" }, { name: "og:type", content: "tabletop-tools" },
{ name: "og:url", content: "ttt.alexcreates.fr" }, { name: "og:url", content: "ttt.alexcreates.fr" },
{ name: 'robots', content: 'noindex, nofollow'} { name: "robots", content: "noindex, nofollow"}
], ],
link: [ link: [
{ rel: 'apple-touch-icon', sizes: "76x76", href: '/apple-touch-icon.png' }, { 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: "32x32", type: "image/png", href: "/favicon-32x32.png" },
{ rel: 'icon', sizes: "16x16", type: 'image/png', href: '/favicon-16x16.png' }, { rel: "icon", sizes: "16x16", type: "image/png", href: "/favicon-16x16.png" },
{ rel: 'manifest', href: '/site.webmanifest' }, { rel: "manifest", href: "/site.webmanifest" },
{ rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#6595b4' }, { rel: "mask-icon", href: "/safari-pinned-tab.svg", color: "#6595b4" },
] ]
}) })
@@ -35,8 +34,24 @@ const useIdFunction = () => useId()
<NuxtLayout> <NuxtLayout>
<ConfigProvider :use-id="useIdFunction"> <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> </ConfigProvider>
</NuxtLayout> </NuxtLayout>
<ClientOnly>
<UiToaster />
</ClientOnly>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.wrapper > * {
height: 100%;
}
</style>

View File

@@ -2,7 +2,8 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root { @layer base {
:root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 84% 4.9%; --foreground: 222.2 84% 4.9%;
@@ -18,8 +19,8 @@
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%; --primary: 245 58% 51%;
--primary-foreground: 210 40% 98%; --primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; --secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 222.2 47.4% 11.2%;
@@ -30,40 +31,49 @@
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--success: 156 72% 67%;
--success-muted: 156 72% 90%;
--success-foreground: 222.2 84% 4.9%;
--ring: 222.2 84% 4.9%; --ring: 222.2 84% 4.9%;
--radius: 0.5rem; --radius: 0.5rem;
} }
:root.dark { .dark {
--background: 222.2 84% 4.9%; --background: 180 0 4.9%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%; --muted: 244 47% 20%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%; --popover: 180 0 4.9%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%; --card: 180 0 4.9%;
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217 0 10%;
--input: 217.2 32.6% 17.5%; --input: 244 47% 20%;
--primary: 210 40% 98%; --primary: 245 58% 51%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%; --secondary: 244 47% 20%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%; --accent: 244 47% 20%;
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--success: 142 72% 29%;
--success-muted: 142 72% 10%;
--success-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%; --ring: 212.7 26.8% 83.9%;
}
} }
@layer base { @layer base {
@@ -74,3 +84,211 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
/* Colors */
.event-red {
@apply text-slate-900 bg-red-500 hover:bg-red-600
}
.event-orange {
@apply text-slate-900 bg-orange-500 hover:bg-orange-600
}
.event-amber {
@apply text-slate-900 bg-amber-500 hover:bg-amber-600
}
.event-yellow {
@apply text-slate-900 bg-yellow-600 hover:bg-yellow-700
}
.event-lime {
@apply text-slate-900 bg-lime-500 hover:bg-lime-600
}
.event-green {
@apply text-white bg-green-600 hover:bg-green-700
}
.event-emerald {
@apply text-white bg-emerald-600 hover:bg-emerald-700
}
.event-teal {
@apply text-white bg-teal-600 hover:bg-teal-700
}
.event-cyan {
@apply text-white bg-cyan-600 hover:bg-cyan-700
}
.event-sky {
@apply text-white bg-sky-600 hover:bg-sky-700
}
.event-blue {
@apply text-white bg-blue-600 hover:bg-blue-700
}
.event-indigo {
@apply text-white bg-indigo-600 hover:bg-indigo-700
}
.event-violet {
@apply text-white bg-violet-600 hover:bg-violet-700
}
.event-purple {
@apply text-white bg-purple-600 hover:bg-purple-700
}
.event-fuchsia {
@apply text-white bg-fuchsia-600 hover:bg-fuchsia-700
}
.event-pink {
@apply text-white bg-pink-700 hover:bg-pink-800
}
.event-rose {
@apply text-white bg-rose-700 hover:bg-rose-800
}
.event-black {
@apply text-white bg-stone-500 hover:bg-stone-700
}
.event-white {
@apply text-slate-900 bg-white hover:bg-yellow-200 border-[1px] border-slate-300
}
.dark .event-details-red {
--base-color: var(--color-red-800);
}
.dark .event-details-orange {
--base-color: var(--color-orange-800);
}
.dark .event-details-amber {
--base-color: var(--color-amber-800);
}
.dark .event-details-yellow {
--base-color: var(--color-yellow-800);
}
.dark .event-details-lime {
--base-color: var(--color-lime-800);
}
.dark .event-details-green {
--base-color: var(--color-green-800);
}
.dark .event-details-emerald {
--base-color: var(--color-emerald-800);
}
.dark .event-details-teal {
--base-color: var(--color-teal-800);
}
.dark .event-details-cyan {
--base-color: var(--color-cyan-800);
}
.dark .event-details-sky {
--base-color: var(--color-sky-800);
}
.dark .event-details-blue {
--base-color: var(--color-blue-800);
}
.dark .event-details-indigo {
--base-color: var(--color-indigo-800);
}
.dark .event-details-violet {
--base-color: var(--color-violet-800);
}
.dark .event-details-purple {
--base-color: var(--color-purple-800);
}
.dark .event-details-fuchsia {
--base-color: var(--color-fuchsia-800);
}
.dark .event-details-pink {
--base-color: var(--color-pink-800);
}
.dark .event-details-rose {
--base-color: var(--color-rose-800);
}
.dark .event-details-black {
--base-color: var(--color-stone-800);
}
.dark .event-details-white {
--base-color: var(--color-white);
}
.event-details-red {
--base-color: var(--color-red-300);
}
.event-details-orange {
--base-color: var(--color-orange-300);
}
.event-details-amber {
--base-color: var(--color-amber-300);
}
.event-details-yellow {
--base-color: var(--color-yellow-300);
}
.event-details-lime {
--base-color: var(--color-lime-300);
}
.event-details-green {
--base-color: var(--color-green-300);
}
.event-details-emerald {
--base-color: var(--color-emerald-300);
}
.event-details-teal {
--base-color: var(--color-teal-300);
}
.event-details-cyan {
--base-color: var(--color-cyan-300);
}
.event-details-sky {
--base-color: var(--color-sky-300);
}
.event-details-blue {
--base-color: var(--color-blue-300);
}
.event-details-indigo {
--base-color: var(--color-indigo-300);
}
.event-details-violet {
--base-color: var(--color-violet-300);
}
.event-details-purple {
--base-color: var(--color-purple-300);
}
.event-details-fuchsia {
--base-color: var(--color-fuchsia-300);
}
.event-details-pink {
--base-color: var(--color-pink-300);
}
.event-details-rose {
--base-color: var(--color-rose-300);
}
.event-details-black {
--base-color: var(--color-stone-300);
}
.event-details-white {
--base-color: var(--color-white);
}
.event-details {
--bg-color: color-mix(in srgb, var(--base-color), var(--color-white) 80%);
--border-color: color-mix(in srgb, var(--base-color), var(--color-white) 50%);
background-color: var(--bg-color);
border-color: var(--border-color);
hr {
border-color: var(--border-color);
}
}
.dark .event-details {
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 80%);
--border-color: color-mix(in srgb, var(--base-color), var(--color-slate-800) 50%);
}
.fade-enter-active,
.fade-leave-active {
transition: all .5s ease;
}
.fade-delay-enter-active,
.fade-delay-leave-active {
transition: all .5s ease 1s;
}
.fade-enter-from,
.fade-leave-to,
.fade-delay-enter-from,
.fade-delay-leave-to {
opacity: 0;
visibility: hidden;
}

View File

@@ -2,6 +2,7 @@
"$schema": "https://shadcn-vue.com/schema.json", "$schema": "https://shadcn-vue.com/schema.json",
"style": "default", "style": "default",
"typescript": true, "typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "assets/main.css", "css": "assets/main.css",

View File

@@ -1,87 +1,39 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { computed, type Component, type ComputedRef } from 'vue' import { computed, type Component, type ComputedRef } from "vue"
import MonthlyLayout from './state/monthly/Layout.vue' // import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import CenturyLayout from './state/centennially/Layout.vue' import MonthlyLayout from "./state/monthly/Layout.vue"
import DecadeLayout from './state/decennially/Layout.vue' import CenturyLayout from "./state/centennially/Layout.vue"
import YearLayout from './state/yearly/Layout.vue' import DecadeLayout from "./state/decennially/Layout.vue"
import YearLayout from "./state/yearly/Layout.vue"
const { isAdvancedSearchOpen, isEditEventModalOpen } = storeToRefs(useCalendar()) import type { Calendar } from "~/models/CalendarConfig"
import type { Category } from "~/models/Category"
const route = useRoute() const props = defineProps<{
const worldId = route.params.id calendarData: Calendar,
categories: Category[]
}>()
const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar() const { setActiveCalendar } = useCalendar()
const { setEvents } = useCalendarEvents() setActiveCalendar(props.calendarData, props.categories)
const { setCharacters } = useCharacters()
const { data: calendar, pending: calPending, refresh: calRefresh } = await useLazyFetch(`/api/calendars/query?world_id=${worldId}`) const { currentConfig, jumpToDate, selectedDate } = useCalendar()
const { data: characters, pending: charPending, refresh: charRefresh } = await useLazyFetch(`/api/characters/query?world_id=${worldId}`) const { isReadOnly } = storeToRefs(useCalendar())
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 currentViewComponent: ComputedRef<Component> = computed<Component>(() => { const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case "month":
return MonthlyLayout return MonthlyLayout
case 'year': case "year":
return YearLayout return YearLayout
case 'decade': case "decade":
return DecadeLayout return DecadeLayout
case 'century': case "century":
default: default:
return CenturyLayout return CenturyLayout
} }
@@ -93,23 +45,34 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="h-full"> <div class="h-full w-full relative">
<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]"> <div class="h-full grid grid-rows-[auto,1fr]">
<CalendarMenu /> <CalendarMenu />
<KeepAlive> <component :is="currentViewComponent" />
<component :is="currentViewComponent"/>
</KeepAlive>
</div>
</template>
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" /> <CalendarSearch />
<CalendarFormUpdateEvent v-model:model-value="isEditEventModalOpen" /> <CalendarDialogUpdateEvent v-if="!isReadOnly" />
<CalendarDialogDeleteEvent v-if="!isReadOnly" />
</div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.screen-move, /* apply transition to moving elements */
.screen-enter-active,
.screen-leave-active {
transition: all .4s ease-out;
}
.screen-enter-from,
.screen-leave-to {
opacity: 0;
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.screen-leave-active {
position: absolute;
}
</style>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { storeToRefs } from 'pinia' import { storeToRefs } from "pinia"
import { computed } from 'vue' import { computed } from "vue"
import { PhMapPin } from '@phosphor-icons/vue' import { PhMapPin } from "@phosphor-icons/vue"
const { defaultDate, getFormattedDateTitle, getRelativeString } = useCalendar() const { defaultDate, getFormattedDateTitle, getRelativeString } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar()) const { selectedDate } = storeToRefs(useCalendar())

View File

@@ -1,65 +1,81 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils' import type { RPGDate } from "@/models/Date"
import type { RPGDate } from '@/models/Date' import type { CalendarEvent } from "~/models/CalendarEvent"
import type { CalendarEvent } from '~/models/CalendarEvent'
const props = defineProps<{ const props = defineProps<{
event: CalendarEvent event: CalendarEvent
tileDate: RPGDate tileDate: RPGDate
}>() }>()
const { areDatesIdentical } = useCalendar() const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent } = storeToRefs(useCalendar())
const spansMultipleDays = Boolean(props.event.startDate && props.event.endDate) const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
const isEndEvent = computed(() => spansMultipleDays.value && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate))
const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, props.event.startDate) const titleCharLimit = 50;
const isEndEvent = const eventTitle = computed<string>(() => props.event.title.length <= titleCharLimit ? props.event.title : `${props.event.title.slice(0, titleCharLimit)}`)
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
// Popover code
const isPopoverDetailsOpen = ref<boolean>(false) const isPopoverDetailsOpen = ref<boolean>(false)
function handleDoubleClick() {
isPopoverDetailsOpen.value = false
lastActiveEvent.value = { ...props.event }
revealEditEventModal()
}
function handleDelete() {
isPopoverDetailsOpen.value = false
lastActiveEvent.value = { ...props.event }
revealDeleteEventModal()
}
function handleClosePopover() { function handleClosePopover() {
isPopoverDetailsOpen.value = false 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> </script>
<template> <template>
<UiPopover v-model:open="isPopoverDetailsOpen"> <UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child> <UiPopoverTrigger as-child>
<button <button
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis" class="text-xs px-2 py-1 block w-full text-left rounded-sm focus-visible:bg-red-200"
:class=" :class="
cn({ cn(
'text-white bg-slate-600 hover:bg-slate-700': !event.category, event.category ? `event-${event.category.color}` : '',
'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', 'italic': event.hidden,
'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-r-none': isStartEvent,
'rounded-l-none': isEndEvent 'rounded-l-none': isEndEvent,
}) }
)
" "
@dblclick="handleDoubleClick"
@keydown.delete="handleDelete"
> >
{{ event.title }} <div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
{{ eventTitle }}
</div>
</button> </button>
</UiPopoverTrigger> </UiPopoverTrigger>
<CalendarEventDetails <LazyCalendarEventDetails
:event :event
:spans-multiple-days :spans-multiple-days
:is-start-event :is-start-event

View File

@@ -1,19 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '@/models/Date' import { cn } from "@/lib/utils"
import type { CalendarEvent } from '@/models/CalendarEvent' import type { RPGDate } from "@/models/Date"
import { useCalendar } from '@/stores/CalendarStore' import type { CalendarEvent } from "@/models/CalendarEvent"
import { useCalendar } from "@/stores/CalendarStore"
import { import {
PhHourglassMedium, PhHourglassMedium,
PhAlarm, PhAlarm,
PhHourglassHigh, PhHourglassHigh,
PhHourglassLow, PhHourglassLow,
PhDotsThreeOutlineVertical PhMapPinArea,
} from '@phosphor-icons/vue' PhDotsThreeOutlineVertical,
PhEye
} from "@phosphor-icons/vue"
const { defaultDate, getFormattedDateTitle, jumpToDate, revealEditEventModal, getRelativeString } = useCalendar() const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { deleteEvent } = useCalendarEvents() const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
const { lastActiveEvent } = storeToRefs(useCalendarEvents())
const props = defineProps<{ const props = defineProps<{
event: CalendarEvent event: CalendarEvent
@@ -25,16 +27,14 @@ const props = defineProps<{
// Ref for the popover // Ref for the popover
const eventDetails = ref<HTMLElement>() const eventDetails = ref<HTMLElement>()
const emit = defineEmits(['query:close-popover']) const emit = defineEmits(["query:close-popover"])
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate) const dateDifference = computed<string>(() => getRelativeString(defaultDate, props.event.startDate))
const dateDuration: string | null = props.event.endDate const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
: null
function handleJumpToDate(date: RPGDate) { function handleJumpToDate(date: RPGDate) {
jumpToDate(date) jumpToDate(date)
emit('query:close-popover') emit("query:close-popover")
} }
/** /**
@@ -46,42 +46,39 @@ function deployEditModal() {
lastActiveEvent.value = { ...props.event } lastActiveEvent.value = { ...props.event }
revealEditEventModal() revealEditEventModal()
commandMenuOpened.value = false commandMenuOpened.value = false
emit("query:close-popover")
}
/**
* Confirm event deletion
*/
function deployDeleteModal() {
lastActiveEvent.value = { ...props.event }
revealDeleteEventModal()
commandMenuOpened.value = false
emit("query:close-popover")
} }
</script> </script>
<template> <template>
<UiPopoverContent <UiPopoverContent
class="event-details w-96 bg-slate-900" class="w-96"
:align="'center'" :align="'center'"
:align-offset="50" :align-offset="50"
:side="'left'" :side="'left'"
:collision-padding="60" :collision-padding="60"
:hide-when-detached="true" :hide-when-detached="true"
:class="{ :class="cn(
'border-slate-800': !event.category, event.category ? `event-details-${event.category.color}` : '',
'border-lime-800': event.category?.name === 'naissance', {
'border-stone-600': event.category?.name === 'mort', 'event-details': event.category
'border-orange-800': event.category?.name === 'catastrophe', }
'border-pink-800': event.category?.name === 'catastrophe naturelle', )"
'border-sky-800': event.category?.name === 'législation', @focus-outside.prevent
'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"> <div ref="eventDetails" class="grid gap-1">
<header class="pr-12"> <header class="pr-12">
<div class="text-lg font-semibold"> <div class="mb-2 max-h-24 overflow-y-auto text-lg font-semibold">
{{ event.title }} {{ event.title }}
</div> </div>
@@ -91,34 +88,54 @@ function deployEditModal() {
</template> </template>
<template v-else> <template v-else>
<p class="font-semibold"> <p class="font-semibold">
Du {{ getFormattedDateTitle(event.startDate, true) }} au {{
{{ getFormattedDateTitle(event.endDate, true) }} $t('entity.calendar.date.fromTo',
{
startDate: getFormattedDateTitle(event.startDate, true),
endDate: getFormattedDateTitle(event.endDate, true)
}
)
}}
</p> </p>
</template> </template>
</div> </div>
</header> </header>
<div class="mb-1 space-y-1"> <div class="mb-1 space-y-1">
<p class="text-sm italic opacity-75 flex items-center gap-1"> <template v-if="event.location">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }} <div class="text-sm italic dark:opacity-75 flex items-center gap-1">
</p> <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"> <template v-if="dateDuration">
<p class="text-sm italic opacity-75 flex items-center gap-1"> <div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }} <div>
</p> <PhHourglassMedium size="16" weight="fill" />
</div>
<p>{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}</p>
</div>
</template> </template>
</div> </div>
<template v-if="event.category || event.secondaryCategories"> <template v-if="event.category || event.secondaryCategories">
<ul class="flex gap-1"> <ul class="flex gap-1">
<li v-if="event.category"> <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-400 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }} {{ event.category?.name }}
</UiBadge> </UiBadge>
</li> </li>
<li v-for="cat in event.secondaryCategories" :key="cat.id"> <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 }} {{ cat.name }}
</UiBadge> </UiBadge>
</li> </li>
@@ -128,16 +145,16 @@ function deployEditModal() {
<template v-if="event.description"> <template v-if="event.description">
<hr class="border-slate-500 mt-2" > <hr class="border-slate-500 mt-2" >
<div class="mt-2 text-sm text-slate-300"> <div class="max-h-48 overflow-y-auto mt-2 text-sm text-slate-600 dark:text-slate-300">
{{ event.description }} {{ event.description }}
</div> </div>
</template> </template>
</div> </div>
<menu class="absolute top-4 right-4"> <menu v-if="!isReadOnly" class="absolute top-4 right-4" :class="cn({ 'top-6': event.hidden })">
<UiPopover v-model:open="commandMenuOpened"> <UiPopover v-model:open="commandMenuOpened">
<UiPopoverTrigger as-child> <UiPopoverTrigger as-child>
<UiButton size="icon" variant="ghost"> <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" /> <PhDotsThreeOutlineVertical size="20" weight="fill" />
</UiButton> </UiButton>
</UiPopoverTrigger> </UiPopoverTrigger>
@@ -145,8 +162,8 @@ function deployEditModal() {
<UiCommand> <UiCommand>
<UiCommandList> <UiCommandList>
<UiCommandGroup> <UiCommandGroup>
<UiCommandItem value="edit-event" @select="deployEditModal"> Modifier </UiCommandItem> <UiCommandItem value="edit-event" @select="deployEditModal"> {{ $t('ui.action.edit') }} </UiCommandItem>
<UiCommandItem value="delete-event" @select="deleteEvent(event.id!)"> Supprimer </UiCommandItem> <UiCommandItem value="delete-event" @select="deployDeleteModal"> {{ $t('ui.action.delete') }} </UiCommandItem>
</UiCommandGroup> </UiCommandGroup>
</UiCommandList> </UiCommandList>
</UiCommand> </UiCommand>
@@ -155,93 +172,39 @@ function deployEditModal() {
</menu> </menu>
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2"> <nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child> <UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child>
<button <button
class="flex gap-1" class="flex gap-1"
title="Naviguer au début" title="Naviguer au début"
@click="handleJumpToDate(event.startDate!)" @click="handleJumpToDate(event.startDate!)"
> >
<PhHourglassHigh size="16" weight="fill" /> Début <PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }}
</button> </button>
</UiBadge> </UiBadge>
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin"> <UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child title="Naviguer à la fin">
<button <button
class="flex gap-1" class="flex gap-1"
title="Naviguer à la fin" title="Naviguer à la fin"
@click="handleJumpToDate(event.endDate!)" @click="handleJumpToDate(event.endDate!)"
> >
<PhHourglassLow size="16" weight="fill" /> Fin <PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }}
</button> </button>
</UiBadge> </UiBadge>
</nav> </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> </UiPopoverContent>
</template> </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>

View File

@@ -1,16 +1,20 @@
<script lang="ts" setup> <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 { revealAdvancedSearch } = useCalendar()
const { isReadOnly } = storeToRefs(useCalendar())
</script> </script>
<template> <template>
<header class="pt-4 border-slate-700 border-b-[1px]"> <header class="pt-4 border-slate-400 dark:border-slate-700 border-b-[1px]">
<div class="px-6 flex justify-between"> <div class="px-6 flex justify-between">
<menu class="flex items-center gap-2"> <menu class="flex items-center gap-2">
<li class="flex items-center"> <li v-if="!isReadOnly">
<CalendarDialogQuickCreateEvent />
</li>
<li>
<CalendarMenuToday /> <CalendarMenuToday />
</li> </li>
<li> <li>
@@ -25,11 +29,13 @@ const { revealAdvancedSearch } = useCalendar()
<li> <li>
<UiButton search-slash @click="revealAdvancedSearch()"> <UiButton search-slash @click="revealAdvancedSearch()">
<PhMagnifyingGlass size="20" weight="light" /> <PhMagnifyingGlass size="20" weight="light" />
Recherche avancée {{ $t('entity.advancedSearch.title') }}
</UiButton> </UiButton>
</li> </li>
<li> <li>
<ClientOnly>
<CalendarSwitch /> <CalendarSwitch />
</ClientOnly>
</li> </li>
</menu> </menu>
</div> </div>

View File

@@ -1,13 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, type ComputedRef } from 'vue' import { computed, type ComputedRef } from "vue"
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { import {
PhCaretDoubleLeft, PhCaretDoubleLeft,
PhCaretDoubleRight, PhCaretDoubleRight,
PhCaretLeft, PhCaretLeft,
PhCaretRight PhCaretRight
} from '@phosphor-icons/vue' } from "@phosphor-icons/vue"
const { t } = useI18n()
interface DirectionLabels { interface DirectionLabels {
pastFar: string pastFar: string
@@ -16,126 +18,126 @@ interface DirectionLabels {
futureFar: string futureFar: string
} }
const { currentConfig, decrementMonth, incrementMonth, decrementYear, incrementYear } = const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } =
useCalendar() useCalendar()
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => { const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case "month":
return { return {
pastFar: 'Année précédente', pastFar: t("entity.calendar.years.prevSingular"),
pastNear: 'Mois précédent', pastNear: t("entity.calendar.months.prevSingular"),
futureNear: 'Mois suivant', futureNear: t("entity.calendar.months.nextSingular"),
futureFar: 'Année suivante' futureFar: t("entity.calendar.years.nextSingular")
} }
case 'year': case "year":
return { return {
pastFar: 'Décennie précédente', pastFar: t("entity.calendar.decades.prevSingular"),
pastNear: 'Année précédente', pastNear: t("entity.calendar.years.prevSingular"),
futureNear: 'Année suivante', futureNear: t("entity.calendar.years.nextSingular"),
futureFar: 'Décennie suivante' futureFar: t("entity.calendar.decades.nextSingular")
} }
case 'decade': case "decade":
return { return {
pastFar: 'Siècle précédent', pastFar: t("entity.calendar.centuries.prevSingular"),
pastNear: 'Décennie précédente', pastNear: t("entity.calendar.decades.prevSingular"),
futureNear: 'Décennie suivante', futureNear: t("entity.calendar.decades.nextSingular"),
futureFar: 'Siècle suivant' futureFar: t("entity.calendar.centuries.nextSingular")
} }
case 'century': case "century":
default: default:
return { return {
pastFar: 'Millénaire précédent', pastFar: t("entity.calendar.millenias.prevSingular"),
pastNear: 'Siècle précédent', pastNear: t("entity.calendar.centuries.prevSingular"),
futureNear: 'Siècle suivant', futureNear: t("entity.calendar.centuries.nextSingular"),
futureFar: 'Millénaire suivant' futureFar: t("entity.calendar.millenias.nextSingular")
} }
} }
}) })
function toPastFar(): void { function toPastFar(): void {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case "month":
decrementYear() decrementViewYear()
break break
case 'year': case "year":
decrementYear(10) decrementViewYear(10)
break break
case 'decade': case "decade":
decrementYear(100) decrementViewYear(100)
break break
case 'century': case "century":
default: default:
decrementYear(1000) decrementViewYear(1000)
break break
} }
} }
function toPastNear(): void { function toPastNear(): void {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case "month":
decrementMonth() decrementViewMonth()
break break
case 'year': case "year":
decrementYear() decrementViewYear()
break break
case 'decade': case "decade":
decrementYear(10) decrementViewYear(10)
break break
case 'century': case "century":
default: default:
decrementYear(100) decrementViewYear(100)
break break
} }
} }
function toFutureNear(): void { function toFutureNear(): void {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case "month":
incrementMonth() incrementViewMonth()
break break
case 'year': case "year":
incrementYear() incrementViewYear()
break break
case 'decade': case "decade":
incrementYear(10) incrementViewYear(10)
break break
case 'century': case "century":
default: default:
incrementYear(100) incrementViewYear(100)
break break
} }
} }
function toFutureFar(): void { function toFutureFar(): void {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case "month":
incrementYear() incrementViewYear()
break break
case 'year': case "year":
incrementYear(10) incrementViewYear(10)
break break
case 'decade': case "decade":
incrementYear(100) incrementViewYear(100)
break break
case 'century': case "century":
default: default:
incrementYear(1000) incrementViewYear(1000)
break break
} }
} }

View File

@@ -1,71 +1,71 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type RPGDate } from '@/models/Date' const { currentDate } = useCalendar()
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue' // const { toast } = useToast()
// const { t } = useI18n()
const { currentDate, currentConfig, jumpToDate } = useCalendar() // function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
const { getRelativeEventFromDate } = useCalendarEvents() // let fromDate: RPGDate
function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') { // // To modify, obviously
let fromDate: RPGDate // const daysPerMonth = 32
// To modify, obviously // const toDay = position === "next" ? daysPerMonth : 1
const daysPerMonth = 32 // const toMonth = position === "next" ? monthsPerYear : 0
const monthsPerYear = 10
const toDay = position === 'next' ? daysPerMonth : 1 // switch (currentConfig.viewType) {
const toMonth = position === 'next' ? monthsPerYear : 0 // case "month":
// fromDate = {
// day: toDay,
// month: currentDate.currentMonth,
// year: currentDate.currentYear
// }
// break
switch (currentConfig.viewType) { // case "year":
case 'month': // fromDate = {
fromDate = { // day: toDay,
day: toDay, // month: toMonth,
month: currentDate.currentMonth, // year: currentDate.currentYear
year: currentDate.currentYear // }
} // break
break
case 'year': // case "decade":
fromDate = { // fromDate = {
day: toDay, // day: toDay,
month: toMonth, // month: currentDate.currentMonth,
year: currentDate.currentYear // year: currentDate.currentYear
} // }
break // break
case 'decade': // case "century":
fromDate = { // default:
day: toDay, // fromDate = {
month: currentDate.currentMonth, // day: toDay,
year: currentDate.currentYear // month: currentDate.currentMonth,
} // year: currentDate.currentYear
break // }
// break
// }
case 'century': // try {
default: // const { targetDate } = getRelativeEventFromDate(fromDate, position)
fromDate = {
day: toDay,
month: currentDate.currentMonth,
year: currentDate.currentYear
}
break
}
try { // jumpToDate(targetDate)
const { targetDate } = getRelativeEventFromDate(fromDate, position) // } catch (err) {
// toast({
jumpToDate(targetDate) // title: t("entity.calendar.event.outOfBoundsTitle"),
} catch (err) { // variant: "default",
console.log(err) // description: t("entity.calendar.event.outOfBoundsMessage"),
} // duration: 4000,
} // })
// }
// }
</script> </script>
<template> <template>
<div class="flex gap-2"> <div class="flex gap-2">
<div class="grid items-end w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm"> <div class="grid items-end w-40 px-4 py-2 border-slate-200 bg-white dark:bg-black dark:border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors">
<ClientOnly> <ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span> <span>{{ currentDate.currentDateTitle }}</span>
@@ -76,43 +76,47 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
</template> </template>
</ClientOnly> </ClientOnly>
</div> </div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm"> <!-- <div>
<UiTooltipProvider :delay-duration="250"> <UiTooltipProvider :delay-duration="250">
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<UiButton <UiButton
variant="ghost" variant="outline"
size="icon" size="icon"
class="rounded-t-sm rounded-b-none" class="rounded-t-sm rounded-b-none border-b-0"
@click="handleGotoPreviousEventPage('prev')" @click="handleGotoPreviousEventPage('prev')"
> >
<PhArrowLineLeft size="22" /> <PhArrowLineLeft size="22" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p>Précédente page à évènements</p> <p>
{{ $t('entity.calendar.event.prevPage') }}
</p>
</UiTooltipContent> </UiTooltipContent>
</UiTooltip> </UiTooltip>
</UiTooltipProvider> </UiTooltipProvider>
</div> </div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm"> <div>
<UiTooltipProvider :delay-duration="250"> <UiTooltipProvider :delay-duration="250">
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<UiButton <UiButton
variant="ghost" variant="outline"
size="icon" size="icon"
class="rounded-t-sm rounded-b-none" class="rounded-t-sm rounded-b-none border-b-0"
@click="handleGotoPreviousEventPage('next')" @click="handleGotoPreviousEventPage('next')"
> >
<PhArrowLineRight size="22" /> <PhArrowLineRight size="22" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p>Prochaine page à évènements</p> <p>
{{ $t('entity.calendar.event.nextPage') }}
</p>
</UiTooltipContent> </UiTooltipContent>
</UiTooltip> </UiTooltip>
</UiTooltipProvider> </UiTooltipProvider>
</div> </div> -->
</div> </div>
</template> </template>

View File

@@ -1,32 +1,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore' import { computed } from "vue"
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const { defaultDate, areDatesIdentical } = useCalendar() const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true) const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
const isDefaultDate: ComputedRef<boolean> = computed<boolean>(() => areDatesIdentical(selectedDate.value, defaultDate)) const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year
})
</script> </script>
<template> <template>
<UiTooltipProvider :delay-duration="250"> <UiTooltipProvider :delay-duration="250">
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<ClientOnly> <UiButton size="sm" variant="secondary" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate"> {{ $t('entity.calendar.date.today') }}
Aujourd'hui
</UiButton> </UiButton>
<template #fallback>
<UiButton size="sm">
Aujourd'hui
</UiButton>
</template>
</ClientOnly>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p>{{ defaultDateFormatted }}</p> <p>{{ defaultDateFormatted }}</p>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { PhCalendarBlank } from '@phosphor-icons/vue' import { PhCalendarBlank } from "@phosphor-icons/vue"
import { computed } from 'vue' import { computed } from "vue"
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar() const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
@@ -18,7 +18,9 @@ const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</UiButton> </UiButton>
</UiDropdownMenuTrigger> </UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30"> <UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
<UiDropdownMenuLabel>Mode d'affichage</UiDropdownMenuLabel> <UiDropdownMenuLabel>
{{ $t('ui.displayMode') }}
</UiDropdownMenuLabel>
<UiDropdownMenuSeparator /> <UiDropdownMenuSeparator />
<UiDropdownMenuItem <UiDropdownMenuItem
v-for="option in viewTypeOptions" v-for="option in viewTypeOptions"

View File

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

View File

@@ -0,0 +1,46 @@
<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")
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6">
<UiAlertDialogTitle>
<span class="text-2xl">
<strong class="font-bold">{{ world.name }}</strong>
<span class="opacity-30"> </span>
<span v-if="calendarSkeletonName">
{{ calendarSkeletonName }}
</span>
<span v-else>
{{ $t('entity.calendar.createDialog.title') }}
</span>
</span>
</UiAlertDialogTitle>
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
<PhX size="20" />
</UiButton>
<CalendarFormCreate @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -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(e: Event) {
e.preventDefault()
popoverOpen.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"
: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="handleClosing"
@pointer-down-outside="handleClosing"
>
<CalendarFormCreateEvent @event-created="handleClosing" />
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -0,0 +1,110 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/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: 3000
})
} 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"
>
<UiAlertDialogTitle>
{{ $t('entity.calendar.deleteDialog.title') }}
</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.calendar.deleteDialog.subtitle') }}
</UiAlertDialogDescription>
<form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-4">
<div class="text-red-500 ml-8">
<span class="text-sm">
<!-- {{ formErrors.message }} -->
</span>
</div>
</div>
<footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="handleClosing">
{{ $t('ui.action.back') }}
</UiButton>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton v-if="calendar" size="sm" variant="destructive" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('entity.deleteOne', { entity: calendar?.name }) }}
</UiButton>
</div>
</footer>
</form>
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -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 = { ...lastActiveEvent.value }
}
})
/**
* Prevents the modal from closing if's still loading
*
* @param e The closing event (can be keydown or click)
*/
function handleClosing(e: Event): void {
if (isLoading.value) {
e.preventDefault()
}
}
</script>
<template>
<UiAlertDialog v-model:open="isDeleteEventModalOpen">
<UiAlertDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="handleClosing"
>
<UiAlertDialogTitle>{{ $t('entity.calendar.event.deleteDialog.title') }}</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.calendar.event.deleteDialog.subtitle') }}
</UiAlertDialogDescription>
<CalendarFormDeleteEvent />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { PhPlus } from "@phosphor-icons/vue";
const isDialogOpen = ref<boolean>(false);
// Toggles the dialog
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
</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">
<UiDialogTitle>
{{ $t("entity.calendar.event.addSingle") }}
</UiDialogTitle>
<CalendarFormCreateEvent @event-created="toggleDialog" />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
// Watch the popover state
watch(isEditEventModalOpen, (hasOpened, _o) => {
if (hasOpened && lastActiveEvent.value) {
eventSkeleton.value = { ...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
}
</script>
<template>
<UiDialog v-model:open="isEditEventModalOpen">
<UiDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="pl-3 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="(e) => e.preventDefault()"
>
<header class="pl-8 grid gap-y-2">
<UiDialogTitle>
{{ $t('entity.calendar.event.editDialog.title') }}
</UiDialogTitle>
<UiDialogDescription>
{{ $t('entity.calendar.event.editDialog.subtitle') }}
</UiDialogDescription>
</header>
<CalendarFormUpdateEvent @event-updated="handleClosing" />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -0,0 +1,135 @@
<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" }
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.createDialog.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.createDialog.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.createDialog.tabs.today.title') }}
</div>
</UiTabsTrigger>
</UiTabsList>
<UiTabsContent value="global">
<input
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full -my-1 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
@input="handleNameChange"
>
</UiTabsContent>
<UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
</UiTabsContent>
<UiTabsContent value="today">
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
</UiTabsContent>
</UiTabs>
<footer class="flex justify-end gap-2 mt-6">
<UiButton type="button" variant="destructive" @click="handleFormCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
<UiButton type="submit" :disabled="!validSkeleton || isCreatingCalendar">
<Transition name="fade">
<PhCircleNotch v-if="isCreatingCalendar" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</footer>
</form>
</template>
</template>

View File

@@ -1,13 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '~/models/Date'; import type { RPGDate } from "~/models/Date";
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue"
import { const emit = defineEmits(["event-created"])
PhAlarm
} from '@phosphor-icons/vue'
const { eventSkeleton } = storeToRefs(useCalendarEvents()) const { eventSkeleton } = storeToRefs(useCalendar())
const { resetSkeleton, submitSkeleton } = useCalendarEvents() const { submitSkeleton, cancelLatestRequest } = useCalendar()
const popoverOpen = ref(false)
const { t } = useI18n()
const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({ const formErrors = reactive<{ message: string | null }>({
message: null message: null
@@ -18,54 +20,39 @@ const props = defineProps<{
btnClass?: string btnClass?: string
}>() }>()
/**
* Opens event creation's popover
*/
function openEventCreatePopover() {
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 (?)
}
}
// Watch the popover state
// And reset the skeleton if it has been dismissed
watch(popoverOpen, (hasOpened, _o) => {
if (!hasOpened) {
resetSkeleton()
}
})
async function handleSubmit() { async function handleSubmit() {
// Prevent form submission if already loading
if (isLoading.value) return
isLoading.value = true
try { try {
await submitSkeleton() await submitSkeleton()
popoverOpen.value = false emit("event-created")
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
formErrors.message = err.message 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> </script>
<template> <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"> <form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-y-4"> <div class="grid grid-cols-2 gap-y-3">
<div class="col-span-2 pl-8"> <div class="col-span-2 pl-8">
<input <input
id="new-event-title" id="new-event-title"
@@ -73,27 +60,37 @@ async function handleSubmit() {
type="text" type="text"
name="new-event-title" name="new-event-title"
required required
placeholder="Titre de l'évènement" :placeholder="$t('entity.calendar.event.title')"
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"> :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-none 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 class="col-span-2 pl-8"> <div class="col-span-2 my-2 pl-8">
<textarea <textarea
id="new-event-description" id="new-event-description"
v-model="eventSkeleton.description" v-model="eventSkeleton.description"
name="new-event-description" name="new-event-description"
placeholder="Description brève de l'évènement" :placeholder="$t('entity.addDescription')"
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" :maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500"
/> />
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-4">
<PhAlarm size="18" weight="fill" /> <PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate <CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate" v-model:model-value="eventSkeleton.startDate"
placeholder="Date de début" :placeholder="$t('entity.calendar.date.start')"
:initial-date="props.date" :initial-date="props.date"
:required="true" :required="true"
/> />
@@ -102,25 +99,93 @@ async function handleSubmit() {
<CalendarInputRPGDate <CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate" v-model:model-value="eventSkeleton.endDate"
placeholder="Date de fin" :placeholder="$t('entity.calendar.date.end')"
:initial-date="props.date" :initial-date="props.date"
/> />
</div> </div>
</div> </div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
:maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }}
</div>
</div>
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
<PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden">
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel>
</div>
</div>
</div>
<div class="text-red-500 pl-8"> <div class="text-red-500 pl-8">
<span class="text-sm"> <span class="text-sm">
{{ formErrors.message }} {{ formErrors.message }}
</span> </span>
</div> </div>
<div class="text-right"> <div class="flex gap-2 justify-end">
<UiButton size="sm"> <Transition name="fade-delay">
Sauvegarder <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> </UiButton>
</div> </div>
</div> </div>
</form> </form>
</UiPopoverContent>
</UiPopover>
</template> </template>

View File

@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/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: 3000
})
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false
}
}
/**
* Click on the cancel button
*
* Must cancel the abortController in the store, and stop the loading
*/
function handleCancel(): void {
cancelLatestRequest()
isLoading.value = false
}
</script>
<template>
<form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-4">
<div class="text-red-500 ml-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
</div>
<footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="() => isDeleteEventModalOpen = false">
{{ $t('ui.action.back') }}
</UiButton>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton size="sm" variant="destructive" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('ui.action.delete') }}
</UiButton>
</div>
</footer>
</form>
</template>

View File

@@ -1,85 +1,113 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhAlarm } from '@phosphor-icons/vue' import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
import { VisuallyHidden } from 'radix-vue' import { useToast } from "~/components/ui/toast";
import type { APIError } from "~/models/Errors";
const isModalOpened = defineModel<boolean>({ default: false }) const emit = defineEmits(["event-updated"])
const { resetSkeleton, updateEventFromSkeleton } = useCalendarEvents()
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendarEvents()) 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 }>({ const formErrors = reactive<{ message: string | null }>({
message: null message: null
}) })
// Watch the popover state async function handleAction() {
watch(isModalOpened, (hasOpened, _o) => { if (isLoading.value) return
if (hasOpened && lastActiveEvent.value) {
eventSkeleton.value = { ...lastActiveEvent.value } isLoading.value = true
} else {
resetSkeleton()
}
})
async function handleSubmit() {
try { try {
await updateEventFromSkeleton() await updateEventFromSkeleton()
isModalOpened.value = false emit("event-updated")
toast({
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
variant: "success",
duration: 3000
})
} catch (err) { } catch (err) {
if (err instanceof Error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
formErrors.message = err.message 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: 2000,
})
})
} 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> </script>
<template> <template>
<UiDialog v-model:open="isModalOpened" :modal="true"> <form @submit.prevent="handleAction">
<UiDialogContent <div class="grid grid-cols-2 gap-y-3">
:align="'center'" <div class="col-span-2">
:side="'right'" <div class="flex items-center gap-4">
:collision-padding="60" <PhPencilSimpleLine size="20" weight="fill" />
: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>
<UiDialogDescription> <div class="grow">
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">
<input <input
id="new-event-title" id="new-event-title"
v-model="eventSkeleton.title" v-model="eventSkeleton.title"
type="text" type="text"
name="new-event-title" name="new-event-title"
required required
placeholder="Titre de l'évènement" :placeholder="$t('entity.calendar.event.title')"
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"> :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-none 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>
<div class="col-span-2 ml-8"> <div class="col-span-2 my-2 ml-8">
<textarea <textarea
id="new-event-description" id="new-event-description"
v-model="eventSkeleton.description" v-model="eventSkeleton.description"
name="new-event-description" name="new-event-description"
placeholder="Description brève de l'évènement" :placeholder="$t('entity.addDescription')"
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" :maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500"
/> />
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-4">
<PhAlarm size="18" weight="fill" /> <PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate <CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate" v-model:model-value="eventSkeleton.startDate"
placeholder="Date de début" :placeholder="$t('entity.calendar.date.start')"
:initial-date="lastActiveEvent?.startDate" :initial-date="lastActiveEvent?.startDate"
:required="true" :required="true"
/> />
@@ -88,25 +116,98 @@ async function handleSubmit() {
<CalendarInputRPGDate <CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate" v-model:model-value="eventSkeleton.endDate"
placeholder="Date de fin" :placeholder="$t('entity.calendar.date.end')"
:initial-date="lastActiveEvent?.endDate" :initial-date="lastActiveEvent?.endDate"
/> />
</div> </div>
</div> </div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div>
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
</div>
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
:minlength="3"
:maxlength="160"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none 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"> <div class="text-red-500 ml-8">
<span class="text-sm"> <span class="text-sm">
{{ formErrors.message }} {{ formErrors.message }}
</span> </span>
</div> </div>
</div>
<div class="text-right"> <footer class="flex gap-2 justify-end">
<UiButton size="sm" type="submit"> <Transition name="fade-delay">
Sauvegarder <UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton> </UiButton>
</div> </Transition>
</div>
<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> </form>
</UiDialogContent>
</UiDialog>
</template> </template>

View 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>

View 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>

View 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] dark:border-slate-800 p-4 rounded-sm max-h-80 overflow-y-auto"
:class="model.length ? 'md:grid md:grid-cols-12 md:gap-4 md:items-center' : ''"
>
<div v-if="model.length" class="hidden md:block col-span-1">
<ul class="grid gap-y-4 justify-center">
<li v-for="(m, i) in model" :key="`num-${m.name}`">
<UiButton size="icon" variant="secondary" class="h-8 w-8 rounded-full">
<span class="font-bold text-sm">{{ i + 1 }}</span>
</UiButton>
</li>
</ul>
</div>
<div class="md:col-span-11">
<ul ref="monthSortableList" class="grid gap-y-2" :class="model.length ? 'visible' : 'absolute invisible'">
<template v-if="model.length">
<li v-for="(m, i) in model" :key="m.name" class="grid md:grid-cols-12 gap-4 md:items-center text-slate-900 bg-slate-200 rounded-md">
<div class="md:col-span-1 text-right duration-200 ease-out transition transform origin-top-right">
<UiButton type="button" variant="ghost" size="icon" class="handle rounded-full h-8 w-8">
<PhList size="17" />
</UiButton>
</div>
<div class="md:col-span-3">
<div class="font-bold md:pl-2">
{{ m.name }}
</div>
</div>
<div class="md:col-span-2">
<UiInput
:id="`month-days-n${i}`"
v-model="m.days"
class="bg-transparent border-none"
type="number"
:name="`monthDays-n${i}`"
:placeholder="$t('entity.calendar.months.daysNb')"
min="0"
/>
</div>
<div class="md:col-span-1">
{{ $t('entity.calendar.months.daysMaybePlural') }}
</div>
<div class="md:col-start-12">
<UiTooltipProvider>
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton type="button" variant="ghost" size="icon" class="rounded-full h-8 w-8" @click="removeMonthFromModel(i)">
<PhTrash size="17" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.months.deleteOne', { month: m.name }) }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</li>
</template>
</ul>
</div>
<template v-if="!model.length">
<p class="col-span-12 text-lg text-center italic opacity-50">
{{ $t('entity.calendar.months.none') }}
</p>
</template>
</div>
</div>
</div>
</template>

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '~/models/Date'; import type { RPGDate } from "~/models/Date";
import { import {
PhXCircle PhXCircle
} from '@phosphor-icons/vue' } from "@phosphor-icons/vue"
const model = defineModel<RPGDate | null>() const model = defineModel<RPGDate | null>()
@@ -27,7 +27,7 @@ const inputPlaceholder = computed<string>(() => {
return props.placeholder return props.placeholder
} }
return '' return ""
}) })
const initialDateValue = props.initialDate ? { ...props.initialDate } : null const initialDateValue = props.initialDate ? { ...props.initialDate } : null
@@ -37,7 +37,7 @@ const monthTitle = computed<string>(() => {
return getMonthName(model.value.month) return getMonthName(model.value.month)
} }
return '' return ""
}) })
const hasValue = computed<boolean>(() => !!model.value) const hasValue = computed<boolean>(() => !!model.value)
@@ -117,7 +117,7 @@ function handleValueReset() {
v-if="model" v-if="model"
align="start" align="start"
side="bottom" 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"> <div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
<UiSelect @update:model-value="handleMonthChange"> <UiSelect @update:model-value="handleMonthChange">
@@ -151,8 +151,11 @@ function handleValueReset() {
<button <button
v-for="day in monthData.days" v-for="day in monthData.days"
:key="`popover-day-grid-${id}-${day}`" :key="`popover-day-grid-${id}-${day}`"
class="aspect-square rounded-full text-[.8em] transition-colors hover:bg-slate-700" class="aspect-square rounded-full text-[.8em] transition-colors"
:class="{'bg-blue-500 hover:bg-blue-700': day === model.day}" :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)" @click="setModelDay(day)"
> >
{{ day }} {{ day }}

View 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>

View File

@@ -2,50 +2,51 @@
import { import {
isCharacter, isCharacter,
type Character, type Character,
} from '@/models/Characters' } from "@/models/Characters"
import type { RPGDateOrder } from '@/models/Date' import type { RPGDateOrder } from "@/models/Date"
import { import {
isCalendarEvent, isCalendarEvent,
type CalendarEvent, type CalendarEvent,
} from '~/models/CalendarEvent' } from "~/models/CalendarEvent"
import { useCharacters } from '@/stores/CharacterStore' import { capitalize } from "@/utils/Strings"
import { useCalendarEvents } from '@/stores/EventStore' import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core"
import { capitalize } from '@/utils/Strings' import { computed, ref, watch } from "vue"
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core' import { searchUnifier, type SearchMode } from "../SearchMode"
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 { import {
ComboboxAnchor, ComboboxAnchor,
ComboboxInput, ComboboxInput,
ComboboxPortal, ComboboxPortal,
ComboboxRoot, ComboboxRoot,
ComboboxTrigger,
VisuallyHidden VisuallyHidden
} from 'radix-vue' } from "radix-vue"
import SearchList from './lists/SearchList.vue' import SearchList from "./lists/SearchList.vue"
import type { Category } from '~/models/Category' import type { Category } from "~/models/Category"
const { isAdvancedSearchOpen, allEvents } = storeToRefs(useCalendar())
const { characters } = storeToRefs(useCharacters()) const { characters } = storeToRefs(useCharacters())
const { allEvents } = storeToRefs(useCalendarEvents())
const modalOpen = defineModel<boolean>({ default: false }) const searchQuery = ref<string>("")
const searchQuery = ref<string>('')
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2) // 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 // Order
const selectedOrder = ref<RPGDateOrder>('asc') const selectedOrder = ref<RPGDateOrder>("asc")
function setOrderAsc() { function setOrderAsc() {
selectedOrder.value = 'asc' selectedOrder.value = "asc"
resetPage() resetPage()
} }
function setOrderDesc() { function setOrderDesc() {
selectedOrder.value = 'desc' selectedOrder.value = "desc"
resetPage() resetPage()
} }
@@ -69,9 +70,9 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Assign data to loop over and filter // Assign data to loop over and filter
// They are assigned this way for readability // They are assigned this way for readability
if (selectedEntity.value === 'events') { if (selectedEntity.value === "events") {
dataToFilter = allEvents.value dataToFilter = allEvents.value
} else if (selectedEntity.value === 'characters') { } else if (selectedEntity.value === "characters") {
dataToFilter = characters.value dataToFilter = characters.value
} else { } else {
dataToFilter = [...allEvents.value, ...characters.value] dataToFilter = [...allEvents.value, ...characters.value]
@@ -84,15 +85,15 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Filter calendar events // Filter calendar events
if (isCalendarEvent(item)) { if (isCalendarEvent(item)) {
const queryString = new String(searchQuery.value) const queryString = new String(searchQuery.value)
.replace(searchUnifier, '') .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
const hitTitle = item.title const hitTitle = item.title
.replace(searchUnifier, '') .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(queryString) .includes(queryString)
const hitDesc = item.description const hitDesc = item.description
?.replace(searchUnifier, '') ?.replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(queryString) .includes(queryString)
@@ -113,7 +114,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
} }
hitCategories = selectedCategories.value.every((selectedCat) => { hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat as Category) return allCategories.findIndex((c) => c.name === selectedCat.name) !== -1
}) })
return (hitTitle || hitDesc) && hitCategories return (hitTitle || hitDesc) && hitCategories
@@ -122,11 +123,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Filter characters // Filter characters
if (isCharacter(item)) { if (isCharacter(item)) {
const queryString = new String(searchQuery.value) const queryString = new String(searchQuery.value)
.replace(searchUnifier, '') .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
const hitTitle = item.name const hitTitle = item.name
.replace(searchUnifier, '') .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(queryString) .includes(queryString)
@@ -161,7 +162,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
* Removes the search query, resets the pagination and removes all selected categories * Removes the search query, resets the pagination and removes all selected categories
*/ */
function resetSearch() { function resetSearch() {
searchQuery.value = '' searchQuery.value = ""
resetPage() resetPage()
selectedCategories.value = [] selectedCategories.value = []
} }
@@ -170,14 +171,14 @@ function resetSearch() {
* Opens the search Uidialog * Opens the search Uidialog
*/ */
function openUiDialog() { function openUiDialog() {
modalOpen.value = true isAdvancedSearchOpen.value = true
} }
/** /**
* Closes the search Uidialog * Closes the search Uidialog
*/ */
function closeUiDialog() { function closeUiDialog() {
modalOpen.value = false isAdvancedSearchOpen.value = false
} }
/** /**
@@ -203,28 +204,54 @@ watch([currentPage, selectedEntity], () => {
}) })
// Compute categories based on current selectedEntity // Compute categories based on current selectedEntity
const currentCategories = computed(() => { const { data: resCategories } = await useFetch("/api/calendars/categories/query")
return [] const currentCategories = ref<Category[]>(resCategories.value?.data as Category[])
})
const selectedCategories = ref<(Category)[]>([]) const selectedCategories = ref<(Category)[]>([])
const categoryFilterOpened = ref<boolean>(false) const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>('') const searchCategory = ref<string>("")
const filteredCategories = computed(() => const filteredCategories = computed(() => {
currentCategories.value.filter((i) => !selectedCategories.value.includes(i)) // 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 * Handles the category selections from the TagInput component
* *
* @param e Radix Change Event * @param e Radix Change Event
*/ */
function handleCategorySelect(e: (Category)) { function handleCategorySelect(e: Category) {
if (typeof e === 'string') { searchCategory.value = ""
searchCategory.value = ''
selectedCategories.value.push(e) 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) { if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false categoryFilterOpened.value = false
@@ -233,7 +260,7 @@ function handleCategorySelect(e: (Category)) {
</script> </script>
<template> <template>
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()"> <UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
<UiDialogContent <UiDialogContent
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]" class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
:class="{ :class="{
@@ -241,11 +268,11 @@ function handleCategorySelect(e: (Category)) {
}" }"
> >
<VisuallyHidden> <VisuallyHidden>
<UiDialogTitle> Recherche avancée </UiDialogTitle> <UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
</VisuallyHidden> </VisuallyHidden>
<VisuallyHidden> <VisuallyHidden>
<UiDialogDescription> <UiDialogDescription>
Rechercher les données disponibles sur le calendrier {{ $t('entity.advancedSearch.title') }}
</UiDialogDescription> </UiDialogDescription>
</VisuallyHidden> </VisuallyHidden>
@@ -256,7 +283,7 @@ function handleCategorySelect(e: (Category)) {
id="search" id="search"
v-model:model-value="searchQuery" v-model:model-value="searchQuery"
type="text" type="text"
placeholder="Rechercher le calendrier" :placeholder="$t('entity.advancedSearch.ctaPlaceholder')"
class="pl-10 py-6 text-lg" class="pl-10 py-6 text-lg"
autocomplete="off" autocomplete="off"
/> />
@@ -265,7 +292,7 @@ function handleCategorySelect(e: (Category)) {
</span> </span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between gap-8">
<div> <div>
<UiToggleGroup <UiToggleGroup
v-model="selectedEntity" v-model="selectedEntity"
@@ -274,20 +301,20 @@ function handleCategorySelect(e: (Category)) {
@update:model-value="handleEntitySwitch()" @update:model-value="handleEntitySwitch()"
> >
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements"> <UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
Évènements {{ $t('entity.calendar.event.namePlural') }}
</UiToggleGroupItem>
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
Personnages
</UiToggleGroupItem> </UiToggleGroupItem>
<!-- Not used for now -->
<!-- <UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
{{ $t('entity.character.namePlural') }}
</UiToggleGroupItem> -->
</UiToggleGroup> </UiToggleGroup>
</div> </div>
<div class="flex items-center gap-1"> <div class="grow flex justify-end items-center gap-1">
<UiTagsInput class="px-0 gap-0 w-72"> <UiTagsInput class="grow px-0 gap-y-1 w-80">
<div class="flex gap-2 flex-wrap items-center px-3"> <div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
<UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name"> <UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name">
<UiTagsInputItemText class="capitalize" /> <UiTagsInputItemText class="capitalize cursor-pointer" @click="handleCategoryUnselect(item)" />
<UiTagsInputItemDelete />
</UiTagsInputItem> </UiTagsInputItem>
</div> </div>
@@ -295,18 +322,23 @@ function handleCategorySelect(e: (Category)) {
v-model="selectedCategories" v-model="selectedCategories"
v-model:open="categoryFilterOpened" v-model:open="categoryFilterOpened"
v-model:searchTerm="searchCategory" v-model:searchTerm="searchCategory"
class="w-full" class="grow flex items-center gap-y-1 pr-2"
> >
<ComboboxAnchor as-child> <ComboboxAnchor as-child>
<ComboboxInput placeholder="Catégories" as-child> <ComboboxInput :placeholder="$t('entity.category.namePlural')" as-child>
<UiTagsInputInput <UiTagsInputInput
class="w-full px-3" ref="categoryInput"
:class="selectedCategories.length > 0 ? 'mt-2' : ''" v-model="categoryInputValue"
class="min-w-16 px-3"
@keydown.enter.prevent @keydown.enter.prevent
/> />
</ComboboxInput> </ComboboxInput>
</ComboboxAnchor> </ComboboxAnchor>
<ComboboxTrigger>
<PhCaretDown size="16" />
</ComboboxTrigger>
<ComboboxPortal :to="'#searchForm'"> <ComboboxPortal :to="'#searchForm'">
<UiCommandList <UiCommandList
position="popper" position="popper"
@@ -317,11 +349,11 @@ function handleCategorySelect(e: (Category)) {
<UiCommandGroup> <UiCommandGroup>
<UiCommandItem <UiCommandItem
v-for="cat in filteredCategories" v-for="cat in filteredCategories"
:key="cat" :key="cat.name"
:value="cat" :value="cat"
@select.prevent="handleCategorySelect(cat)" @select.prevent="handleCategorySelect(cat)"
> >
{{ capitalize(cat) }} {{ capitalize(cat.name) }}
</UiCommandItem> </UiCommandItem>
</UiCommandGroup> </UiCommandGroup>
</UiCommandList> </UiCommandList>
@@ -341,7 +373,7 @@ function handleCategorySelect(e: (Category)) {
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p>Plus ancien</p> <p>{{ $t('entity.advancedSearch.older') }}</p>
</UiTooltipContent> </UiTooltipContent>
</UiTooltip> </UiTooltip>
</UiTooltipProvider> </UiTooltipProvider>
@@ -358,7 +390,7 @@ function handleCategorySelect(e: (Category)) {
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p>Plus récent</p> <p>{{ $t('entity.advancedSearch.newer') }}</p>
</UiTooltipContent> </UiTooltipContent>
</UiTooltip> </UiTooltip>
</UiTooltipProvider> </UiTooltipProvider>

View File

@@ -1,18 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Character } from '@/models/Characters' import type { Character } from "@/models/Characters"
import type { RPGDate } from '@/models/Date' import type { RPGDate } from "@/models/Date"
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { PhArrowSquareOut, PhPlant, PhSkull } from '@phosphor-icons/vue' import { PhArrowSquareOut, PhPlant, PhSkull } from "@phosphor-icons/vue"
const props = defineProps<{ const props = defineProps<{
character: Character character: Character
}>() }>()
defineEmits<{ defineEmits<{
(e: 'query:date-jump', payload: RPGDate): void (e: "query:date-jump", payload: RPGDate): void
}>() }>()
const { getFormattedDateTitle } = useCalendar() const { getFormattedDateTitle } = useCalendar()

View File

@@ -1,56 +1,36 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '@/models/Date' import { cn } from "@/lib/utils"
import type { CalendarEvent } from '@/models/CalendarEvent' import type { RPGDate } from "@/models/Date"
import { useCalendar } from '@/stores/CalendarStore' 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<{ const props = defineProps<{
event: CalendarEvent event: CalendarEvent
}>() }>()
defineEmits<{ defineEmits<{
(e: 'query:date-jump', payload: RPGDate): void (e: "query:date-jump", payload: RPGDate): void
}>() }>()
const { getRelativeString } = useCalendar() const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar()
const { defaultDate, getFormattedDateTitle } = useCalendar()
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate) const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
const dateDuration: string | null = props.event.endDate const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
: null
</script> </script>
<template> <template>
<button <button
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors" class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
:class="{ :class="cn(
'text-white bg-slate-600 hover:bg-slate-700': !event.category, event.category ? `event-${event.category.color}` : '',
'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', 'pt-4': event.hidden,
'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'
}"
@click="$emit('query:date-jump', event.startDate)" @click="$emit('query:date-jump', event.startDate)"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center mb-1">
<h2 class="font-bold"> <h2 class="font-bold text-lg">
{{ event.title }} {{ event.title }}
</h2> </h2>
<div v-if="event.wiki"> <div v-if="event.wiki">
@@ -63,27 +43,54 @@ const dateDuration: string | null = props.event.endDate
</div> </div>
</div> </div>
<div class="mb-1"> <div class="flex gap-2 items-center justify-between mb-1">
<template v-if="!event.endDate"> <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) }} {{ getFormattedDateTitle(event.startDate, true) }}
</p> </p>
</template> </template>
<template v-else> <template v-else>
<p class="col-span-2 font-semibold text-sm opacity-75"> <p class="font-semibold text-sm opacity-75">
Du {{ getFormattedDateTitle(event.startDate, true) }} au {{
{{ getFormattedDateTitle(event.endDate, true) }} $t('entity.calendar.date.fromTo',
{
startDate: getFormattedDateTitle(event.startDate, true),
endDate: getFormattedDateTitle(event.endDate, true)
}
)
}}
</p> </p>
</template> </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>
<div class="mb-1 flex gap-x-2 items-center"> <div class="mb-1 flex gap-x-2 items-center">
<template v-if="event.location">
<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"> <p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }} <PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p> </p>
<template v-if="dateDuration"> <template v-if="dateDuration">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-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> </p>
</template> </template>
</div> </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"> <div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
<ul class="flex gap-1"> <ul class="flex gap-1">
<li v-if="event.category"> <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 }} {{ event.category?.name }}
</UiBadge> </UiBadge>
</li> </li>
<li v-for="cat in event.secondaryCategories" :key="cat.id"> <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 }} {{ cat.name }}
</UiBadge> </UiBadge>
</li> </li>

View File

@@ -1,13 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isCharacter, type Character } from '@/models/Characters' import { isCharacter, type Character } from "@/models/Characters"
import type { RPGDate, RPGDateOrder } from '@/models/Date' import type { RPGDate, RPGDateOrder } from "@/models/Date"
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { computed } from 'vue' import { computed } from "vue"
import { isCalendarEvent, type CalendarEvent } from '~/models/CalendarEvent' import { isCalendarEvent, type CalendarEvent } from "~/models/CalendarEvent"
import type { SearchMode } from '../../SearchMode' import type { SearchMode } from "../../SearchMode"
import CharacterCallout from './CharacterCallout.vue' import CharacterCallout from "./CharacterCallout.vue"
import EventCallout from './EventCallout.vue' import EventCallout from "./EventCallout.vue"
const props = defineProps<{ const props = defineProps<{
results: (Character | CalendarEvent)[] results: (Character | CalendarEvent)[]
@@ -18,7 +18,7 @@ const props = defineProps<{
limit?: number limit?: number
}>() }>()
const emit = defineEmits(['jumpedToDate']) const emit = defineEmits(["jumpedToDate"])
const { jumpToDate, compareDates } = useCalendar() const { jumpToDate, compareDates } = useCalendar()
@@ -26,7 +26,7 @@ function handleJumpToDate(date?: RPGDate) {
if (!date) return if (!date) return
jumpToDate(date) jumpToDate(date)
emit('jumpedToDate') emit("jumpedToDate")
} }
// Initial sorting of the results // Initial sorting of the results

View File

@@ -1,13 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '@/models/Date' import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from '@/models/CalendarEvent' import type { CalendarEvent } from "@/models/CalendarEvent"
import { useCalendar } from '@/stores/CalendarStore' import { useElementBounding } from "@vueuse/core"
import { useCalendarEvents } from '@/stores/EventStore' import { storeToRefs } from "pinia"
import { useElementBounding } from '@vueuse/core' import { computed, ref, type ComputedRef } from "vue"
import { storeToRefs } from 'pinia'
import { computed, ref, type ComputedRef } from 'vue'
import CalendarEventButton from '../../CalendarEvent.vue' import CalendarEventButton from "../../CalendarEvent.vue"
const props = defineProps<{ const props = defineProps<{
date: RPGDate date: RPGDate
@@ -18,8 +16,7 @@ const calendarTile = ref()
const calendarEventsList = ref() const calendarEventsList = ref()
const { defaultDate, selectDate, areDatesIdentical } = useCalendar() const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar()) const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
const { currentEvents } = storeToRefs(useCalendarEvents())
/** /**
* All events with a startDate / endDate that starts or ends on the tile * 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> <template>
<div <div
ref="calendarTile" ref="calendarTile"
class="tile relative text-xs p-2 border-slate-700" class="tile relative text-xs p-2 border-slate-400 dark:border-slate-700 dark:bg-black transition-colors"
:class="{ :class="{
'text-slate-500': props.faded, 'text-slate-300 dark:text-slate-500': props.faded,
'text-slate-300': !props.faded 'text-slate-500 dark:text-slate-300': !props.faded
}" }"
> >
<button <button
@@ -88,10 +85,10 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
> >
<ClientOnly> <ClientOnly>
<span <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="{ :class="{
'bg-slate-800': isDefaultDate && !isSelectedDate, 'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-blue-500': isSelectedDate 'text-white bg-indigo-500': isSelectedDate
}" }"
> >
{{ date.day }} {{ date.day }}
@@ -108,7 +105,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
}" }"
> >
<TransitionGroup name="event"> <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" /> <CalendarEventButton :event :tile-date="date" />
</li> </li>
</TransitionGroup> </TransitionGroup>
@@ -146,7 +143,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
</ClientOnly> </ClientOnly>
<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> </ClientOnly>
</div> </div>
</template> </template>
@@ -165,8 +162,10 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
.event-enter-active { .event-enter-active {
transition: all 120ms ease-in-out; transition: all 120ms ease-in-out;
} }
.event-move,
.event-leave-active { .event-leave-active {
transition: all 180ms ease-in-out; position: absolute;
transition: all 200ms ease-in-out;
} }
.event-enter-from, .event-enter-from,
.event-leave-to { .event-leave-to {

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from '@vueuse/core' import { useThrottleFn } from "@vueuse/core"
const { currentDate, decrementMonth, incrementMonth } = useCalendar() const { currentDate, decrementViewMonth, incrementViewMonth } = useCalendar()
const { currentMonthData } = storeToRefs(useCalendar()) const { currentMonthData } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) { function handleWheel(e: WheelEvent) {
@@ -15,18 +15,19 @@ function handleWheel(e: WheelEvent) {
} }
const moveCalendarLeft = useThrottleFn(() => { const moveCalendarLeft = useThrottleFn(() => {
decrementMonth() decrementViewMonth()
}, 100) }, 100)
const moveCalendarRight = useThrottleFn(() => { const moveCalendarRight = useThrottleFn(() => {
incrementMonth() incrementViewMonth()
}, 100) }, 100)
</script> </script>
<template> <template>
<div class="grid grid-cols-10" @wheel="handleWheel"> <div class="grid grid-cols-10" @wheel="handleWheel">
<template v-if="currentMonthData">
<CalendarStateMonthlyDayTile <CalendarStateMonthlyDayTile
v-for="day in currentMonthData.days" v-for="day in currentMonthData?.days"
:key="`layout-month-grid-${day}`" :key="`layout-month-grid-${day}`"
:date="{ :date="{
day: day, day: day,
@@ -34,5 +35,6 @@ const moveCalendarRight = useThrottleFn(() => {
year: currentDate.currentYear year: currentDate.currentYear
}" }"
/> />
</template>
</div> </div>
</template> </template>

View File

@@ -1,21 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '@/models/Date' import type { RPGDate } from "@/models/Date"
import { useCalendar } from '@/stores/CalendarStore' import { storeToRefs } from "pinia"
import { useCalendarEvents } from '@/stores/EventStore' import { computed, type ComputedRef } from "vue"
import { storeToRefs } from 'pinia'
import { computed, type ComputedRef } from 'vue'
const { currentDate, defaultDate, selectDate } = useCalendar() const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar()) const { selectedDate, currentEvents } = storeToRefs(useCalendar())
const { currentEvents } = storeToRefs(useCalendarEvents())
const props = defineProps<{ const props = defineProps<{
monthNumber: number monthNumber: number
dayNumber: number dayNumber: number
}>() }>()
const { areDatesIdentical } = useCalendar()
const tileDate: ComputedRef<RPGDate> = computed(() => { const tileDate: ComputedRef<RPGDate> = computed(() => {
return { return {
day: props.dayNumber, day: props.dayNumber,
@@ -45,14 +40,14 @@ const hasAtLeastOneEvent = computed<boolean>(() => {
<button <button
class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors" class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors"
:class="{ :class="{
'hover:bg-slate-800 hover:border-slate-800': !isDefaultDate && !isSelectedDate, 'text-slate-500 hover:border-indigo-300 hover:text-slate-900': !isDefaultDate && !isSelectedDate,
'bg-slate-800 hover:bg-slate-600 hover:border-slate-600': isDefaultDate && !isSelectedDate, 'font-semibold text-white bg-slate-600 dark:bg-slate-800 hover:border-indigo-400': isDefaultDate && !isSelectedDate,
'text-white bg-blue-500 hover:bg-blue-600 hover:border-blue-600': isSelectedDate, 'font-semibold text-white bg-indigo-500 hover:bg-indigo-600 hover:border-indigo-500': isSelectedDate,
'after:bg-green-600': hasAtLeastOneEvent, 'after:bg-green-600': hasAtLeastOneEvent,
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate 'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate
}" }"
@click="selectDate(tileDate)" @click="selectDate(tileDate)"
> >
<span ref="tileRef" class="text-slate-300 text-[.85em]">{{ dayNumber }}</span> <span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span>
</button> </button>
</template> </template>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from '@vueuse/core' import { useThrottleFn } from "@vueuse/core"
const { decrementYear, incrementYear } = useCalendar() const { decrementViewYear, incrementViewYear } = useCalendar()
const { sortedMonths: months } = storeToRefs(useCalendar()) const { sortedMonths: months } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) { function handleWheel(e: WheelEvent) {
@@ -15,11 +15,11 @@ function handleWheel(e: WheelEvent) {
} }
const moveCalendarLeft = useThrottleFn(() => { const moveCalendarLeft = useThrottleFn(() => {
decrementYear() decrementViewYear()
}, 100) }, 100)
const moveCalendarRight = useThrottleFn(() => { const moveCalendarRight = useThrottleFn(() => {
incrementYear() incrementViewYear()
}, 100) }, 100)
</script> </script>

View File

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

View File

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

View File

@@ -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>

View File

@@ -1,11 +0,0 @@
export interface MenuItem {
phIcon: Component
tooltip: string
clickHandler?: () => void
to?: string
}
export interface SidebarProps {
menuItems: MenuItem[],
isHome?: boolean
}

View 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>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { type RPGColor, rpgColors } from "~/models/Color";
const model = defineModel<RPGColor>()
</script>
<template>
<UiSelect v-model="model">
<UiSelectTrigger>
<UiSelectValue :placeholder="$t('ui.colors.selectOne')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectGroup>
<UiSelectItem v-for="color in rpgColors" :key="color" :value="color">
{{ $t(`ui.colors.${color}`) }}
</UiSelectItem>
</UiSelectGroup>
</UiSelectContent>
</UiSelect>
</template>

View File

@@ -0,0 +1,68 @@
<script lang="ts" setup>
import { PhCompass, PhList } from "@phosphor-icons/vue"
import type { SidebarMenuActionType } from "./SidebarProps";
const { revealAdvancedSearch } = useCalendar()
const { currentMenu } = storeToRefs(useUiStore())
// const user = useSupabaseUser()
function handleMenuItemAction(actionType: SidebarMenuActionType) {
if (actionType === "event-search") {
revealAdvancedSearch()
}
}
</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">
<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>
<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="item.phIcon" size="24" weight="fill" />
</RouterLink>
</UiButton>
<UiButton v-if="item.action" variant="ghost" size="icon" class="rounded-full" @click="handleMenuItemAction(item.action!)">
<component :is="item.phIcon" size="24" weight="fill" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>{{ item.tooltip }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
</menu>
<UserCTA />
</nav>
</template>

View File

@@ -0,0 +1,15 @@
import type { ShallowRef } from "vue"
export type SidebarMenuActionType = "event-search"
export interface SidebarMenuItem {
phIcon: ShallowRef // use shallowRef to build phIcon
tooltip: string
action?: SidebarMenuActionType
to?: string
}
export interface SidebarProps {
menuItems: SidebarMenuItem[],
isHome?: boolean
}

View File

@@ -1,13 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from "vue"
import { PhUserCircle } from '@phosphor-icons/vue' import { PhCheckCircle, PhGear, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
import { cn } from "~/lib/utils";
const router = useRouter() const router = useRouter()
const { auth } = useSupabaseClient() const { auth } = useSupabaseClient()
const user = useSupabaseUser() const user = useSupabaseUser()
const userMeta = computed(() => user.value?.user_metadata) const userMeta = computed(() => user.value?.user_metadata)
const profileUrl: string = `${useRequestURL().origin}/my/`
const { locale, setLocale } = useI18n()
const menuOpened = ref<boolean>(false) const menuOpened = ref<boolean>(false)
@@ -19,12 +23,13 @@ watch(user, closeMenu)
async function handleGoogleLogin() { async function handleGoogleLogin() {
try { try {
auth.signInWithOAuth({ auth.signInWithOAuth({
provider: 'google', provider: "google",
options: { options: {
queryParams: { queryParams: {
access_type: 'offline', access_type: "offline",
prompt: 'consent' prompt: "consent"
} },
redirectTo: profileUrl
} }
}) })
} catch (err) { } catch (err) {
@@ -42,40 +47,166 @@ async function handleLogout() {
} }
} }
function gotoProfilePage() { type AvailableRoutes = "/my" | "/my/settings"
router.push({ path: '/profile' })
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
} }
</script> </script>
<template> <template>
<ClientOnly> <ClientOnly>
<UiPopover v-model:open="menuOpened"> <UiDropdownMenu v-model:open="menuOpened">
<UiPopoverTrigger> <UiDropdownMenuTrigger>
<UiAvatar v-if="user" class="cursor-pointer"> <UiAvatar v-if="user" class="cursor-pointer">
<UiAvatarImage <UiAvatarImage
:src="userMeta?.avatar_url" :src="userMeta?.avatar_url"
:alt="userMeta?.full_name" :alt="userMeta?.full_name"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />
<UiAvatarFallback>Me</UiAvatarFallback> <UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar> </UiAvatar>
<UiButton v-else variant="outline" size="icon"> <UiButton v-else variant="outline" size="icon" class="rounded-full">
<PhUserCircle size="18" /> <PhUserCircle size="24" />
</UiButton> </UiButton>
</UiPopoverTrigger> </UiDropdownMenuTrigger>
<UiPopoverContent class="w-fit p-0" :align="'start'" :side="'top'" :collision-padding="20">
<UiCommand> <UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
<UiCommandList v-if="user"> <template v-if="user">
<UiCommandGroup :heading="`Connecté en tant que ${user?.email}`"> <p class="p-2 text-[.7em] opacity-75">
<UiCommandItem value="profile" @select="gotoProfilePage"> Profil </UiCommandItem> {{ $t('ui.greeting', { user: user?.email }) }}
<UiCommandItem value="logout" @select="handleLogout"> Déconnexion </UiCommandItem> </p>
</UiCommandGroup>
</UiCommandList> <UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
<UiCommandList v-else> <PhUser size="20" weight="fill" />
<UiCommandItem value="logout" @select="handleGoogleLogin"> Connexion </UiCommandItem> <span>
</UiCommandList> {{ $t('ui.sidebarMenu.profile') }}
</UiCommand> </span>
</UiPopoverContent> </UiDropdownMenuItem>
</UiPopover>
<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> </ClientOnly>
</template> </template>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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/80 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 bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>

View 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>

View 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>

View File

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

View 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>

View 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>

View 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"

View File

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

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue' import { AvatarFallback, type AvatarFallbackProps } from "radix-vue"
const props = defineProps<AvatarFallbackProps>() const props = defineProps<AvatarFallbackProps>()
</script> </script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from 'radix-vue' import { AvatarImage, type AvatarImageProps } from "radix-vue"
const props = defineProps<AvatarImageProps>() const props = defineProps<AvatarImageProps>()
</script> </script>

View File

@@ -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 Avatar } from "./Avatar.vue"
export { default as AvatarImage } from './AvatarImage.vue' export { default as AvatarImage } from "./AvatarImage.vue"
export { default as AvatarFallback } from './AvatarFallback.vue' export { default as AvatarFallback } from "./AvatarFallback.vue"
export const avatarVariant = cva( export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', "inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
{ {
variants: { variants: {
size: { size: {
sm: 'h-10 w-10 text-xs', sm: "h-10 w-10 text-xs",
base: 'h-16 w-16 text-2xl', base: "h-16 w-16 text-2xl",
lg: 'h-32 w-32 text-5xl', lg: "h-32 w-32 text-5xl",
}, },
shape: { shape: {
circle: 'rounded-full', circle: "rounded-full",
square: 'rounded-md', square: "rounded-md",
}, },
}, },
}, },

View File

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

View File

@@ -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( 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-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{ {
variants: { variants: {
variant: { variant: {
default: default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: 'text-foreground', outline: "text-foreground",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
}, },
}, },
) )

View File

@@ -1,18 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { Primitive, type PrimitiveProps } from 'radix-vue' import { Primitive, type PrimitiveProps } from "radix-vue"
import { type ButtonVariants, buttonVariants } from '.' import { type ButtonVariants, buttonVariants } from "."
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
interface Props extends PrimitiveProps { interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant'] variant?: ButtonVariants["variant"]
size?: ButtonVariants['size'] size?: ButtonVariants["size"]
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
searchSlash?: boolean searchSlash?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
as: 'button' as: "button"
}) })
</script> </script>

View File

@@ -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( 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-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90', default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', outline: "border border-input hover:bg-accent hover:text-accent-foreground",
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: 'hover:bg-accent hover:text-accent-foreground', ghost: "hover:bg-accent hover:text-accent-foreground",
link: 'text-primary underline-offset-4 hover:underline' link: "text-primary underline-offset-4 hover:underline"
}, },
size: { size: {
default: 'h-10 px-4 py-2', default: "h-10 px-4 py-2",
xs: 'h-7 rounded-sm px-2 text-xs', xs: "h-7 rounded-sm px-2 text-xs",
sm: 'h-9 rounded-md px-3 text-sm', sm: "h-9 rounded-md px-3 text-sm",
lg: 'h-11 rounded-md px-8', lg: "h-11 rounded-md px-8",
icon: 'h-10 w-10' icon: "h-10 w-10"
} }
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default' size: "default"
} }
} }
) )

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
link?: string link?: string
}>() }>()
</script> </script>
@@ -11,7 +11,7 @@ const props = defineProps<{
<template> <template>
<div <div
:class=" :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-sm transition-all isolate', props.class, {
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]': 'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
props.link props.link
}) })
@@ -22,7 +22,7 @@ const props = defineProps<{
<NuxtLink <NuxtLink
v-if="props.link" v-if="props.link"
:to="props.link" :to="props.link"
class="absolute inset-0 focus-visible:outline-none" class="absolute inset-0 z-10 focus-visible:outline-none"
/> />
</div> </div>
</template> </template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue' import type { ComboboxRootEmits, ComboboxRootProps } from "radix-vue"
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue' import { ComboboxRoot, useForwardPropsEmits } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), { const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes["class"] }>(), {
open: true, open: true,
modelValue: '' modelValue: ""
}) })
const emits = defineEmits<ComboboxRootEmits>() const emits = defineEmits<ComboboxRootEmits>()

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useForwardPropsEmits } from 'radix-vue' import { useForwardPropsEmits } from "radix-vue"
import type { DialogRootEmits, DialogRootProps } from 'radix-vue' import type { DialogRootEmits, DialogRootProps } from "radix-vue"
import Command from './Command.vue' import Command from "./Command.vue"
import { Dialog, DialogContent } from '@/components/ui/dialog' import { Dialog, DialogContent } from "@/components/ui/dialog"
const props = defineProps<DialogRootProps>() const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>() const emits = defineEmits<DialogRootEmits>()

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import type { ComboboxEmptyProps } from 'radix-vue' import type { ComboboxEmptyProps } from "radix-vue"
import { ComboboxEmpty } from 'radix-vue' import { ComboboxEmpty } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _, ...delegated } = props const { class: _, ...delegated } = props

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import type { ComboboxGroupProps } from 'radix-vue' import type { ComboboxGroupProps } from "radix-vue"
import { ComboboxGroup, ComboboxLabel } from 'radix-vue' import { ComboboxGroup, ComboboxLabel } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps< const props = defineProps<
ComboboxGroupProps & { ComboboxGroupProps & {
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
heading?: string heading?: string
} }
>() >()

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { Search } from 'lucide-vue-next' import { Search } from "lucide-vue-next"
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue' import { ComboboxInput, type ComboboxInputProps, useForwardProps } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
defineOptions({ defineOptions({
inheritAttrs: false inheritAttrs: false
@@ -10,7 +10,7 @@ defineOptions({
const props = defineProps< const props = defineProps<
ComboboxInputProps & { ComboboxInputProps & {
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
} }
>() >()

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue' import type { ComboboxItemEmits, ComboboxItemProps } from "radix-vue"
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue' import { ComboboxItem, useForwardPropsEmits } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<ComboboxItemEmits>() const emits = defineEmits<ComboboxItemEmits>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue' import type { ComboboxContentEmits, ComboboxContentProps } from "radix-vue"
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue' import { ComboboxContent, useForwardPropsEmits } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = withDefaults( const props = withDefaults(
defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(), defineProps<ComboboxContentProps & { class?: HTMLAttributes["class"] }>(),
{ {
dismissable: false dismissable: false
} }

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import type { ComboboxSeparatorProps } from 'radix-vue' import type { ComboboxSeparatorProps } from "radix-vue"
import { ComboboxSeparator } from 'radix-vue' import { ComboboxSeparator } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _, ...delegated } = props const { class: _, ...delegated } = props

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

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

View File

@@ -4,7 +4,7 @@ import {
type DialogRootEmits, type DialogRootEmits,
type DialogRootProps, type DialogRootProps,
useForwardPropsEmits useForwardPropsEmits
} from 'radix-vue' } from "radix-vue"
const props = defineProps<DialogRootProps>() const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>() const emits = defineEmits<DialogRootEmits>()

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'radix-vue' import { DialogClose, type DialogCloseProps } from "radix-vue"
const props = defineProps<DialogCloseProps>() const props = defineProps<DialogCloseProps>()
</script> </script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { import {
DialogClose, DialogClose,
DialogContent, DialogContent,
@@ -8,11 +8,11 @@ import {
DialogOverlay, DialogOverlay,
DialogPortal, DialogPortal,
useForwardPropsEmits useForwardPropsEmits
} from 'radix-vue' } from "radix-vue"
import { X } from 'lucide-vue-next' import { X } from "lucide-vue-next"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>() const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'radix-vue' import { DialogDescription, type DialogDescriptionProps, useForwardProps } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _, ...delegated } = props const { class: _, ...delegated } = props

View File

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

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"]
}>() }>()
</script> </script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { import {
DialogClose, DialogClose,
DialogContent, DialogContent,
@@ -8,11 +8,11 @@ import {
DialogOverlay, DialogOverlay,
DialogPortal, DialogPortal,
useForwardPropsEmits useForwardPropsEmits
} from 'radix-vue' } from "radix-vue"
import { X } from 'lucide-vue-next' import { X } from "lucide-vue-next"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>() const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'radix-vue' import { DialogTitle, type DialogTitleProps, useForwardProps } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _, ...delegated } = props const { class: _, ...delegated } = props

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue' import { DialogTrigger, type DialogTriggerProps } from "radix-vue"
const props = defineProps<DialogTriggerProps>() const props = defineProps<DialogTriggerProps>()
</script> </script>

View File

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

View File

@@ -4,7 +4,7 @@ import {
type DropdownMenuRootEmits, type DropdownMenuRootEmits,
type DropdownMenuRootProps, type DropdownMenuRootProps,
useForwardPropsEmits useForwardPropsEmits
} from 'radix-vue' } from "radix-vue"
const props = defineProps<DropdownMenuRootProps>() const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>() const emits = defineEmits<DropdownMenuRootEmits>()

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { import {
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits, type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps, type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator, DropdownMenuItemIndicator,
useForwardPropsEmits useForwardPropsEmits
} from 'radix-vue' } from "radix-vue"
import { Check } from 'lucide-vue-next' import { Check } from "lucide-vue-next"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>() const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { import {
DropdownMenuContent, DropdownMenuContent,
type DropdownMenuContentEmits, type DropdownMenuContentEmits,
type DropdownMenuContentProps, type DropdownMenuContentProps,
DropdownMenuPortal, DropdownMenuPortal,
useForwardPropsEmits useForwardPropsEmits
} from 'radix-vue' } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = withDefaults( const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(), defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{ {
sideOffset: 4 sideOffset: 4
} }

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue' import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue"
const props = defineProps<DropdownMenuGroupProps>() const props = defineProps<DropdownMenuGroupProps>()
</script> </script>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue' import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps< const props = defineProps<
DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean } DropdownMenuItemProps & { class?: HTMLAttributes["class"]; inset?: boolean }
>() >()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue' import { type HTMLAttributes, computed } from "vue"
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue' import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from "radix-vue"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const props = defineProps< const props = defineProps<
DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean } DropdownMenuLabelProps & { class?: HTMLAttributes["class"]; inset?: boolean }
>() >()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

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