166 Commits

Author SHA1 Message Date
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
Alexis
46f2b0bbed Merge branch 'features/nuxt' into dev 2024-06-05 13:32:57 +02:00
Alexis
be4eb1a7c0 Merge branch 'features/nuxt-supabase' into features/nuxt 2024-06-05 13:32:51 +02:00
Alexis
c4b5ad724c Added delete functions for events 2024-06-04 18:47:54 +02:00
Alexis
373484f17c Added transition to updating event 2024-06-04 18:31:26 +02:00
Alexis
5f6a8e46c6 Added update route for event 2024-06-04 12:01:19 +02:00
Alexis
9802266b8c Added reactivity when adding an event 2024-06-03 21:39:19 +02:00
Alexis
5332b4c9c4 Updated form component
I'll need to refactor this to a Calendar level component
2024-06-02 10:35:18 +02:00
Alexis
a92edd6215 Added post component for events
This needs RLS disabled, will work on that later, I don't like postgre
2024-06-01 11:07:07 +02:00
Alexis
f2d5277c5f Merge branch 'features/nuxt-supabase' into features/nuxt 2024-05-24 20:03:21 +02:00
Alexis
1b801504f2 Reworked relative date operations
This is the most straightforward way to deal with months that _can_ be not the same length. I'm tired
2024-05-24 18:06:58 +02:00
Alexis
c0940c4985 Working event base 2024-05-22 19:27:43 +02:00
Alexis
030e3cdc12 Fixed negative number issue 2024-05-22 19:25:28 +02:00
Alexis
f361fdeb49 Fixed issue with past dates
Some past dates would render as the wrong year packet number
2024-05-20 18:55:09 +02:00
Alexis
a1d0ae0f35 Fixed getRelativeString function
We don't need to have a precise day for display (for now) but it sure would be nice to have
2024-05-20 18:17:13 +02:00
Alexis
784b5cf421 Fixed compareDate function for flexible months
This breaks the year layout, but it's 100% caused by the props of the screen, it's not caused by the date comparison
2024-05-20 12:07:13 +02:00
Alexis
ce2dbbc8a4 Added dummy data as temp bandaid 2024-05-18 21:30:12 +02:00
Alexis
13b8be9399 This broke most things tbh, the date functions need to be reworked to account for differently formatted calendars (how idk). This would fix most issues that stem from it in other screens 2024-05-18 20:56:38 +02:00
Alexis
73e4b0e3d7 Added default date config in calendar data 2024-05-18 18:01:49 +02:00
Alexis
3de59116aa Added secondary categories to events / characters 2024-05-18 17:23:27 +02:00
Alexis
471df5e559 Fixed loading error with calendar store 2024-05-18 15:45:17 +02:00
Alexis
059edd6fc1 Added loading logic to calendar page 2024-05-17 20:50:45 +02:00
Alexis
5dd06ea3f8 Fixed errors caused by new event model 2024-05-17 17:03:56 +02:00
Alexis
67f5a270af Huge refactor for backend and client
This is barely functionnal, but at least it can display some data without crashing
Most other functionnalities other than displaying events are broken, and so are the relative date operations, they need to be fixed asap
2024-05-16 22:58:19 +02:00
Alexis
49e523485b Remodeled database and api to prepare for move 2024-05-15 17:18:17 +02:00
Alexis
3a8e8b0947 Adjusted role permissions 2024-05-14 12:29:31 +02:00
Alexis
0588cb775e Merge branch 'features/nuxt-auth' into features/nuxt 2024-05-14 12:07:11 +02:00
222 changed files with 9667 additions and 2496 deletions

35
app.vue
View File

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

View File

@@ -2,7 +2,8 @@
@tailwind components;
@tailwind utilities;
:root {
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
@@ -18,8 +19,8 @@
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--primary: 245 58% 51%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
@@ -30,40 +31,49 @@
--destructive: 0 84.2% 60.2%;
--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%;
--radius: 0.5rem;
}
:root.dark {
--background: 222.2 84% 4.9%;
.dark {
--background: 180 0 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted: 244 47% 20%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover: 180 0 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card: 180 0 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--border: 217 0 10%;
--input: 244 47% 20%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--primary: 245 58% 51%;
--primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%;
--secondary: 244 47% 20%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent: 244 47% 20%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--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%;
}
}
@layer base {
@@ -74,3 +84,211 @@
@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",
"style": "default",
"typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/main.css",

View File

@@ -1,32 +1,40 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { computed, type Component, type ComputedRef } from 'vue'
import { useCalendar } from "@/stores/CalendarStore"
import { computed, type Component, type ComputedRef } from "vue"
import CalendarMenu from './CalendarMenu.vue'
// import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import MonthlyLayout from "./state/monthly/Layout.vue"
import CenturyLayout from "./state/centennially/Layout.vue"
import DecadeLayout from "./state/decennially/Layout.vue"
import YearLayout from "./state/yearly/Layout.vue"
import MonthlyLayout from './state/monthly/Layout.vue'
import CenturyLayout from './state/centennially/Layout.vue'
import DecadeLayout from './state/decennially/Layout.vue'
import YearLayout from './state/yearly/Layout.vue'
import type { Calendar } from "~/models/CalendarConfig"
import type { Category } from "~/models/Category"
const { currentConfig } = useCalendar()
const { selectedDate, jumpToDate } = useCalendar()
const props = defineProps<{
calendarData: Calendar,
categories: Category[]
}>()
const { fetchEvents } = useCalendarEvents()
fetchEvents()
const { setActiveCalendar } = useCalendar()
setActiveCalendar(props.calendarData, props.categories)
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
// const { setCharacters } = useCharacters()
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) {
case 'month':
case "month":
return MonthlyLayout
case 'year':
case "year":
return YearLayout
case 'decade':
case "decade":
return DecadeLayout
case 'century':
case "century":
default:
return CenturyLayout
}
@@ -38,11 +46,34 @@ onMounted(() => {
</script>
<template>
<div class="h-full w-full relative">
<div class="h-full grid grid-rows-[auto,1fr]">
<CalendarMenu />
<KeepAlive>
<component :is="currentViewComponent" />
</KeepAlive>
<CalendarSearch />
<CalendarFormUpdateEvent />
<CalendarFormDeleteEvent />
</div>
</div>
</template>
<style lang="scss" scoped>
.screen-move, /* apply transition to moving elements */
.screen-enter-active,
.screen-leave-active {
transition: all .4s ease-out;
}
.screen-enter-from,
.screen-leave-to {
opacity: 0;
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.screen-leave-active {
position: absolute;
}
</style>

View File

@@ -1,14 +1,14 @@
<script lang="ts" setup>
import { getRelativeString } from '@/models/Date'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { storeToRefs } from "pinia"
import { computed } from "vue"
import { PhMapPin } from '@phosphor-icons/vue'
import { PhMapPin } from "@phosphor-icons/vue"
const { defaultDate, getFormattedDateTitle } = useCalendar()
const { defaultDate, getFormattedDateTitle, getRelativeString } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
</script>

View File

@@ -1,66 +1,81 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { areDatesIdentical, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events'
import CalendarEventDetails from './CalendarEventDetails.vue'
import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "~/models/CalendarEvent"
const props = defineProps<{
event: CalendarEvent
tileDate: LeimDate
tileDate: RPGDate
}>()
const spansMultipleDays = Boolean(props.event.startDate && props.event.endDate)
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent } = storeToRefs(useCalendar())
const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, props.event.startDate)
const isEndEvent =
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, 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 isPopoverOpen = ref<boolean>(false)
const titleCharLimit = 50;
const eventTitle = computed<string>(() => props.event.title.length <= titleCharLimit ? props.event.title : `${props.event.title.slice(0, titleCharLimit)}`)
// Popover code
const isPopoverDetailsOpen = ref<boolean>(false)
function handleDoubleClick() {
isPopoverDetailsOpen.value = false
lastActiveEvent.value = { ...props.event }
revealEditEventModal()
}
function handleDelete() {
isPopoverDetailsOpen.value = false
lastActiveEvent.value = { ...props.event }
revealDeleteEventModal()
}
function handleClosePopover() {
isPopoverOpen.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>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
class="text-xs px-2 py-1 block w-full text-left rounded-sm focus-visible:bg-red-200"
:class="
cn({
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce',
cn(
event.category ? `event-${event.category.color}` : '',
{
'italic': event.hidden,
'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>
</UiPopoverTrigger>
<CalendarEventDetails
v-once
<LazyCalendarEventDetails
:event
:spans-multiple-days
:is-start-event

View File

@@ -1,20 +1,21 @@
<script lang="ts" setup>
import { getRelativeString, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent"
import { useCalendar } from "@/stores/CalendarStore"
import {
PhHourglassMedium,
PhAlarm,
PhHourglassHigh,
PhHourglassLow,
PhArrowBendDoubleUpLeft,
PhArrowBendDoubleUpRight
} from '@phosphor-icons/vue'
PhMapPinArea,
PhDotsThreeOutlineVertical,
PhEye
} from "@phosphor-icons/vue"
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
const { getRelativeEventFromEvent } = useCalendarEvents()
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent } = storeToRefs(useCalendar())
const props = defineProps<{
event: CalendarEvent
@@ -23,63 +24,58 @@ const props = defineProps<{
isEndEvent?: boolean
}>()
const emit = defineEmits<{
(e: 'query:close-popover'): void
}>()
// Ref for the popover
const eventDetails = ref<HTMLElement>()
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
const dateDuration: string | null = props.event.endDate
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
: null
const emit = defineEmits(["query:close-popover"])
function handleJumpToDate(date: LeimDate) {
const dateDifference = computed<string>(() => getRelativeString(defaultDate, props.event.startDate))
const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
function handleJumpToDate(date: RPGDate) {
jumpToDate(date)
emit('query:close-popover')
emit("query:close-popover")
}
function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
try {
const { targetDate } = getRelativeEventFromEvent(props.event, position, props.isEndEvent)
/**
* Edit event
*/
const commandMenuOpened = ref(false)
handleJumpToDate(targetDate)
} catch (err) {
console.log(err)
}
function deployEditModal() {
lastActiveEvent.value = { ...props.event }
revealEditEventModal()
commandMenuOpened.value = false
}
/**
* Confirm event deletion
*/
function deployDeleteModal() {
lastActiveEvent.value = { ...props.event }
revealDeleteEventModal()
commandMenuOpened.value = false
}
</script>
<template>
<UiPopoverContent
class="event-details w-96 bg-slate-900"
class=" w-96 "
:align="'center'"
:align-offset="50"
:side="'left'"
:collision-padding="60"
:hide-when-detached="true"
:class="{
'border-slate-800': !event.category,
'border-lime-800': event.category === 'naissance',
'border-stone-600': event.category === 'mort',
'border-orange-800': event.category === 'catastrophe',
'border-pink-800': event.category === 'catastrophe naturelle',
'border-sky-800': event.category === 'législation',
'border-purple-800': event.category === 'religion',
'border-emerald-800': event.category === 'joueurs',
'border-amber-800': event.category === 'inauguration',
'border-green-800': event.category === 'invention',
'border-cyan-800': event.category === 'science',
'border-slate-600': event.category === 'bénédiction',
'border-purple-700': event.category === 'découverte',
'border-indigo-700': event.category === 'exploration',
'border-amber-700': event.category === 'construction',
'border-violet-700': event.category === 'arcanologie',
'border-rose-800': event.category === 'criminalité',
'border-stone-700': event.category === 'scandale',
'border-yellow-600': event.category === 'commerce'
}"
:class="cn(
event.category ? `event-details-${event.category.color}` : '',
{
'event-details': event.category
}
)"
>
<div class="grid gap-1">
<div class="text-lg font-semibold">
<div ref="eventDetails" class="grid gap-1">
<header class="pr-12">
<div class="mb-2 max-h-24 overflow-y-auto text-lg font-semibold">
{{ event.title }}
</div>
@@ -89,34 +85,55 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
</template>
<template v-else>
<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>
</template>
</div>
</header>
<div class="mb-1 space-y-1">
<p class="text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p>
<template v-if="event.location">
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhMapPinArea size="16" weight="fill" />
</div>
<p class="max-h-12 overflow-y-auto">{{ event.location }}</p>
</div>
</template>
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhAlarm size="16" weight="fill" />
</div>
<p>{{ dateDifference }}</p>
</div>
<template v-if="dateDuration">
<p class="text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
</p>
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhHourglassMedium size="16" weight="fill" />
</div>
<p>{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}</p>
</div>
</template>
</div>
<template v-if="event.category || event.secondaryCategories">
<ul class="flex gap-1">
<li v-if="event.category">
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
{{ event.category }}
<UiBadge class="mix-blend-luminosity font-bold bg-gray-400 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }}
</UiBadge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat">
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat }}
<li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ cat.name }}
</UiBadge>
</li>
</ul>
@@ -125,121 +142,66 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
<template v-if="event.description">
<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 }}
</div>
</template>
</div>
<nav v-if="event.startDate && event.endDate" class="absolute -top-2 right-2 flex gap-2">
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
<menu class="absolute top-4 right-4" :class="cn({ 'top-6': event.hidden })">
<UiPopover v-model:open="commandMenuOpened">
<UiPopoverTrigger as-child>
<UiButton size="icon" variant="outline" class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600">
<PhDotsThreeOutlineVertical size="20" weight="fill" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent class="w-fit p-0" align="start" side="right" :collision-padding="20" >
<UiCommand>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem value="edit-event" @select="deployEditModal"> {{ $t('ui.action.edit') }} </UiCommandItem>
<UiCommandItem value="delete-event" @select="deployDeleteModal"> {{ $t('ui.action.delete') }} </UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</menu>
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child>
<button
class="flex gap-1"
title="Naviguer au début"
@click="handleJumpToDate(event.startDate!)"
>
<PhHourglassHigh size="16" weight="fill" /> Début
<PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }}
</button>
</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
class="flex gap-1"
title="Naviguer à la fin"
@click="handleJumpToDate(event.endDate!)"
>
<PhHourglassLow size="16" weight="fill" /> Fin
<PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }}
</button>
</UiBadge>
</nav>
<nav class="mt-2 flex gap-2">
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
<button
class="flex gap-1"
title="Évènement précédent"
@click="handleGotoRelativeEvent('prev')"
>
<PhArrowBendDoubleUpLeft size="16" weight="fill" /> Précédent
</button>
<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>
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
<button
class="flex gap-1"
title="Évènement suivant"
@click="handleGotoRelativeEvent('next')"
>
<PhArrowBendDoubleUpRight size="16" weight="fill" /> Suivant
</button>
</UiBadge>
</nav>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.event.hiddenTooltip') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</UiPopoverContent>
</template>
<style lang="scss">
.border-slate-800 {
--base-color: var(--color-slate-800);
}
.border-lime-800 {
--base-color: var(--color-lime-800);
}
.border-stone-600 {
--base-color: var(--color-stone-600);
}
.border-orange-800 {
--base-color: var(--color-orange-800);
}
.border-pink-800 {
--base-color: var(--color-pink-800);
}
.border-sky-800 {
--base-color: var(--color-sky-800);
}
.border-purple-800 {
--base-color: var(--color-purple-800);
}
.border-emerald-800 {
--base-color: var(--color-emerald-800);
}
.border-amber-800 {
--base-color: var(--color-amber-800);
}
.border-green-800 {
--base-color: var(--color-green-800);
}
.border-cyan-800 {
--base-color: var(--color-cyan-800);
}
.border-slate-600 {
--base-color: var(--color-slate-600);
}
.border-purple-700 {
--base-color: var(--color-purple-700);
}
.border-indigo-700 {
--base-color: var(--color-indigo-700);
}
.border-amber-700 {
--base-color: var(--color-amber-700);
}
.border-violet-700 {
--base-color: var(--color-violet-700);
}
.border-rose-800 {
--base-color: var(--color-rose-800);
}
.border-stone-700 {
--base-color: var(--color-stone-700);
}
.border-yellow-600 {
--base-color: var(--color-yellow-600);
}
.event-details {
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 85%);
background-color: var(--bg-color);
hr {
border-color: var(--base-color);
}
}
</style>

View File

@@ -1,18 +1,13 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendar } from "@/stores/CalendarStore"
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import CalendarMenuNav from './CalendarMenuNav.vue'
import CalendarMenuSubnav from './CalendarMenuSubnav.vue'
import CalendarMenuToday from './CalendarMenuToday.vue'
import CalendarSwitch from './CalendarSwitch.vue'
import CalendarCurrentDate from './CalendarCurrentDate.vue'
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
const { revealAdvancedSearch } = useCalendar()
</script>
<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">
<menu class="flex items-center gap-2">
<li class="flex items-center">
@@ -30,11 +25,13 @@ const { revealAdvancedSearch } = useCalendar()
<li>
<UiButton search-slash @click="revealAdvancedSearch()">
<PhMagnifyingGlass size="20" weight="light" />
Recherche avancée
{{ $t('entity.advancedSearch.title') }}
</UiButton>
</li>
<li>
<ClientOnly>
<CalendarSwitch />
</ClientOnly>
</li>
</menu>
</div>

View File

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

View File

@@ -1,20 +1,22 @@
<script lang="ts" setup>
import { daysPerMonth, monthsPerYear, type LeimDate } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { type RPGDate } from "@/models/Date"
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue'
import { PhArrowLineLeft, PhArrowLineRight } from "@phosphor-icons/vue"
const { currentDate, currentConfig, jumpToDate } = useCalendar()
const { getRelativeEventFromDate } = useCalendarEvents()
const { currentDate, currentConfig, jumpToDate, getRelativeEventFromDate } = useCalendar()
function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
let fromDate: LeimDate
const toDay = position === 'next' ? daysPerMonth : 1
const toMonth = position === 'next' ? monthsPerYear : 0
function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
let fromDate: RPGDate
// To modify, obviously
const daysPerMonth = 32
const monthsPerYear = 10
const toDay = position === "next" ? daysPerMonth : 1
const toMonth = position === "next" ? monthsPerYear : 0
switch (currentConfig.viewType) {
case 'month':
case "month":
fromDate = {
day: toDay,
month: currentDate.currentMonth,
@@ -22,7 +24,7 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
}
break
case 'year':
case "year":
fromDate = {
day: toDay,
month: toMonth,
@@ -30,7 +32,7 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
}
break
case 'decade':
case "decade":
fromDate = {
day: toDay,
month: currentDate.currentMonth,
@@ -38,7 +40,7 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
}
break
case 'century':
case "century":
default:
fromDate = {
day: toDay,
@@ -60,7 +62,7 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
<template>
<div class="flex gap-2">
<div class="grid items-end w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm">
<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>
<span>{{ currentDate.currentDateTitle }}</span>
@@ -71,40 +73,44 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
</template>
</ClientOnly>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<div>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
variant="outline"
size="icon"
class="rounded-t-sm rounded-b-none"
class="rounded-t-sm rounded-b-none border-b-0"
@click="handleGotoPreviousEventPage('prev')"
>
<PhArrowLineLeft size="22" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Précédente page à évènements</p>
<p>
{{ $t('entity.calendar.event.prevPage') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<div>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
variant="outline"
size="icon"
class="rounded-t-sm rounded-b-none"
class="rounded-t-sm rounded-b-none border-b-0"
@click="handleGotoPreviousEventPage('next')"
>
<PhArrowLineRight size="22" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Prochaine page à évènements</p>
<p>
{{ $t('entity.calendar.event.nextPage') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>

View File

@@ -1,34 +1,22 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { computed } from "vue"
import { areDatesIdentical } from '@/models/Date'
const { defaultDate } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
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>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<ClientOnly>
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate">
Aujourd'hui
<UiButton size="sm" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
{{ $t('entity.calendar.date.today') }}
</UiButton>
<template #fallback>
<UiButton size="sm">
Aujourd'hui
</UiButton>
</template>
</ClientOnly>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>

View File

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

View File

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

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,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,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: []}
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

@@ -0,0 +1,240 @@
<script lang="ts" setup>
import type { RPGDate } from "~/models/Date";
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue"
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendar()
const popoverOpen = ref(false)
const { t } = useI18n()
const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
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 (?)
}
}
async function handleSubmit() {
// Prevent form submission if already loading
if (isLoading.value) return
isLoading.value = true
try {
await submitSkeleton()
popoverOpen.value = false
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} 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(e: Event) {
if (isLoading.value) {
e.preventDefault()
}
}
/**
* Click on the cancel button
*
* Must cancel the abortController in the store, and stop the loading
*/
function handleCancel() {
cancelLatestRequest()
isLoading.value = false
}
</script>
<template>
<UiPopover v-model:open="popoverOpen">
<UiPopoverTrigger as-child>
<button :class="btnClass" @click="openEventCreatePopover()" />
</UiPopoverTrigger>
<UiPopoverContent
:align="'center'"
:side="'right'"
:collision-padding="60"
: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"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="handleClosing"
>
<form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-y-3">
<div class="col-span-2 pl-8">
<input
id="new-event-title"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
:placeholder="$t('entity.calendar.event.title')"
:maxlength="120"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-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 class="col-span-2 my-2 pl-8">
<textarea
id="new-event-description"
v-model="eventSkeleton.description"
name="new-event-description"
:placeholder="$t('entity.addDescription')"
:maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-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 class="col-span-2">
<div class="flex items-center gap-4">
<PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate"
:placeholder="$t('entity.calendar.date.start')"
:initial-date="props.date"
:required="true"
/>
<span></span>
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate"
:placeholder="$t('entity.calendar.date.end')"
:initial-date="props.date"
/>
</div>
</div>
<div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
:maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-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">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton size="sm" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</div>
</div>
</form>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -0,0 +1,123 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast";
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
const { toast } = useToast()
const { t } = useI18n()
const isLoading = ref<boolean>(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
// Watch the popover state
watch(isDeleteEventModalOpen, (hasOpened, _o) => {
if (hasOpened && lastActiveEvent.value) {
eventSkeleton.value = { ...lastActiveEvent.value }
}
})
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
}
}
/**
* 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()
}
}
/**
* 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>
<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>
<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>
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

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

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

View File

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

@@ -1,54 +1,52 @@
<script lang="ts" setup>
import {
characterCategories,
isCharacter,
type Character,
type CharacterCategory
} from '@/models/Characters'
import type { LeimDateOrder } from '@/models/Date'
} from "@/models/Characters"
import type { RPGDateOrder } from "@/models/Date"
import {
calendarEventCategories,
isCalendarEvent,
type CalendarEvent,
type CalendarEventCategory
} from '@/models/Events'
import { useCharacters } from '@/stores/CharacterStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { capitalize } from '@/utils/Strings'
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { searchUnifier, type SearchMode } from '../SearchMode'
} from "~/models/CalendarEvent"
import { capitalize } from "@/utils/Strings"
import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core"
import { computed, ref, watch } from "vue"
import { searchUnifier, type SearchMode } from "../SearchMode"
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
import { PhCaretDown, PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from "@phosphor-icons/vue"
import {
ComboboxAnchor,
ComboboxInput,
ComboboxPortal,
ComboboxRoot,
ComboboxTrigger,
VisuallyHidden
} from 'radix-vue'
} from "radix-vue"
import SearchList from './lists/SearchList.vue'
import SearchList from "./lists/SearchList.vue"
import type { Category } from "~/models/Category"
const { isAdvancedSearchOpen, allEvents } = storeToRefs(useCalendar())
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 selectedEntity = useStorage('se', 'events' as SearchMode)
// If the query changes, resets the pagination
// This prevents active page to be greater than the search results
watch(searchQuery, resetPage)
const selectedEntity = useStorage("se", "events" as SearchMode)
// Order
const selectedOrder = ref<LeimDateOrder>('asc')
const selectedOrder = ref<RPGDateOrder>("asc")
function setOrderAsc() {
selectedOrder.value = 'asc'
selectedOrder.value = "asc"
resetPage()
}
function setOrderDesc() {
selectedOrder.value = 'desc'
selectedOrder.value = "desc"
resetPage()
}
@@ -72,9 +70,9 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Assign data to loop over and filter
// They are assigned this way for readability
if (selectedEntity.value === 'events') {
if (selectedEntity.value === "events") {
dataToFilter = allEvents.value
} else if (selectedEntity.value === 'characters') {
} else if (selectedEntity.value === "characters") {
dataToFilter = characters.value
} else {
dataToFilter = [...allEvents.value, ...characters.value]
@@ -87,15 +85,15 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Filter calendar events
if (isCalendarEvent(item)) {
const queryString = new String(searchQuery.value)
.replace(searchUnifier, '')
.replace(searchUnifier, "")
.toLocaleLowerCase()
const hitTitle = item.title
.replace(searchUnifier, '')
.replace(searchUnifier, "")
.toLocaleLowerCase()
.includes(queryString)
const hitDesc = item.description
?.replace(searchUnifier, '')
?.replace(searchUnifier, "")
.toLocaleLowerCase()
.includes(queryString)
@@ -105,7 +103,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
const allCategories: CalendarEventCategory[] = []
const allCategories: Category[] = []
if (item.category) {
allCategories.push(item.category)
@@ -116,7 +114,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat as CalendarEventCategory)
return allCategories.findIndex((c) => c.name === selectedCat.name) !== -1
})
return (hitTitle || hitDesc) && hitCategories
@@ -125,11 +123,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Filter characters
if (isCharacter(item)) {
const queryString = new String(searchQuery.value)
.replace(searchUnifier, '')
.replace(searchUnifier, "")
.toLocaleLowerCase()
const hitTitle = item.name
.replace(searchUnifier, '')
.replace(searchUnifier, "")
.toLocaleLowerCase()
.includes(queryString)
@@ -139,7 +137,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
const allCategories: CharacterCategory[] = []
const allCategories: Category[] = []
if (item.category) {
allCategories.push(item.category)
@@ -150,7 +148,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat as CharacterCategory)
return allCategories.includes(selectedCat as Category)
})
return hitTitle && hitCategories
@@ -164,7 +162,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
* Removes the search query, resets the pagination and removes all selected categories
*/
function resetSearch() {
searchQuery.value = ''
searchQuery.value = ""
resetPage()
selectedCategories.value = []
}
@@ -173,14 +171,14 @@ function resetSearch() {
* Opens the search Uidialog
*/
function openUiDialog() {
modalOpen.value = true
isAdvancedSearchOpen.value = true
}
/**
* Closes the search Uidialog
*/
function closeUiDialog() {
modalOpen.value = false
isAdvancedSearchOpen.value = false
}
/**
@@ -206,32 +204,54 @@ watch([currentPage, selectedEntity], () => {
})
// Compute categories based on current selectedEntity
const currentCategories = computed(() => {
if (selectedEntity.value === 'characters') {
return [...characterCategories]
} else {
return [...calendarEventCategories]
}
const { data: resCategories } = await useFetch("/api/calendars/categories/query")
const currentCategories = ref<Category[]>(resCategories.value?.data as Category[])
const selectedCategories = ref<(Category)[]>([])
const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>("")
const filteredCategories = computed(() => {
// Display original data
if (!currentCategories.value) return []
// If current categories are selected, ignore them
if (!searchCategory.value) return currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
// If we also have a query to filter them by, do that
return currentCategories.value.filter((i) => {
return !selectedCategories.value.includes(i) && i.name.toLocaleLowerCase().includes(searchCategory.value.toLocaleLowerCase())
})
})
const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([])
const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>('')
// Reactivity rules for category searches
const categoryInput = ref(null)
const categoryInputValue = ref<string>("")
const { focused: categoryInputFocused } = useFocus(categoryInput)
const filteredCategories = computed(() =>
currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
)
watch(categoryInputFocused, (isFocused) => {
categoryFilterOpened.value = isFocused
})
/**
* Handles the category selections from the TagInput component
*
* @param e Radix Change Event
*/
function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
if (typeof e === 'string') {
searchCategory.value = ''
function handleCategorySelect(e: Category) {
searchCategory.value = ""
selectedCategories.value.push(e)
if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false
}
}
/**
* Removes a category from selection from the TagInput component
*/
function handleCategoryUnselect(e: Category) {
selectedCategories.value.splice(selectedCategories.value.findIndex(sc => sc.name === e.name), 1)
if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false
@@ -240,7 +260,7 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
</script>
<template>
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()">
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
<UiDialogContent
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
:class="{
@@ -248,11 +268,11 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
}"
>
<VisuallyHidden>
<UiDialogTitle> Recherche avancée </UiDialogTitle>
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
</VisuallyHidden>
<VisuallyHidden>
<UiDialogDescription>
Rechercher les données disponibles sur le calendrier
{{ $t('entity.advancedSearch.title') }}
</UiDialogDescription>
</VisuallyHidden>
@@ -263,7 +283,7 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
id="search"
v-model:model-value="searchQuery"
type="text"
placeholder="Rechercher le calendrier"
:placeholder="$t('entity.advancedSearch.ctaPlaceholder')"
class="pl-10 py-6 text-lg"
autocomplete="off"
/>
@@ -272,7 +292,7 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center justify-between gap-8">
<div>
<UiToggleGroup
v-model="selectedEntity"
@@ -281,20 +301,20 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
@update:model-value="handleEntitySwitch()"
>
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
Évènements
</UiToggleGroupItem>
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
Personnages
{{ $t('entity.calendar.event.namePlural') }}
</UiToggleGroupItem>
<!-- Not used for now -->
<!-- <UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
{{ $t('entity.character.namePlural') }}
</UiToggleGroupItem> -->
</UiToggleGroup>
</div>
<div class="flex items-center gap-1">
<UiTagsInput class="px-0 gap-0 w-72" :model-value="selectedCategories">
<div class="flex gap-2 flex-wrap items-center px-3">
<UiTagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
<UiTagsInputItemText class="capitalize" />
<UiTagsInputItemDelete />
<div class="grow flex justify-end items-center gap-1">
<UiTagsInput class="grow px-0 gap-y-1 w-80">
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
<UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name">
<UiTagsInputItemText class="capitalize cursor-pointer" @click="handleCategoryUnselect(item)" />
</UiTagsInputItem>
</div>
@@ -302,18 +322,23 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
v-model="selectedCategories"
v-model:open="categoryFilterOpened"
v-model:searchTerm="searchCategory"
class="w-full"
class="grow flex items-center gap-y-1 pr-2"
>
<ComboboxAnchor as-child>
<ComboboxInput placeholder="Catégories" as-child>
<ComboboxInput :placeholder="$t('entity.category.namePlural')" as-child>
<UiTagsInputInput
class="w-full px-3"
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
ref="categoryInput"
v-model="categoryInputValue"
class="min-w-16 px-3"
@keydown.enter.prevent
/>
</ComboboxInput>
</ComboboxAnchor>
<ComboboxTrigger>
<PhCaretDown size="16" />
</ComboboxTrigger>
<ComboboxPortal :to="'#searchForm'">
<UiCommandList
position="popper"
@@ -324,11 +349,11 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
<UiCommandGroup>
<UiCommandItem
v-for="cat in filteredCategories"
:key="cat"
:key="cat.name"
:value="cat"
@select.prevent="handleCategorySelect(cat)"
>
{{ capitalize(cat) }}
{{ capitalize(cat.name) }}
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
@@ -348,7 +373,7 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Plus ancien</p>
<p>{{ $t('entity.advancedSearch.older') }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
@@ -365,7 +390,7 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Plus récent</p>
<p>{{ $t('entity.advancedSearch.newer') }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>

View File

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

View File

@@ -1,89 +1,96 @@
<script lang="ts" setup>
import { getRelativeString, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events'
import { useCalendar } from '@/stores/CalendarStore'
import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent"
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm } from '@phosphor-icons/vue'
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue"
const props = defineProps<{
event: CalendarEvent
}>()
defineEmits<{
(e: 'query:date-jump', payload: LeimDate): void
(e: "query:date-jump", payload: RPGDate): void
}>()
const { defaultDate, getFormattedDateTitle } = useCalendar()
const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar()
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
const dateDuration: string | null = props.event.endDate
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
: null
const dateDuration = computed<string | null>(() => props.event.endDate ? getRelativeString(props.event.startDate, props.event.endDate, "compact") : null)
</script>
<template>
<button
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
:class="{
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce'
}"
:class="cn(
event.category ? `event-${event.category.color}` : '',
{
'pt-4': event.hidden,
})"
@click="$emit('query:date-jump', event.startDate)"
>
<div class="flex gap-2 items-center">
<h2 class="font-bold">
<div class="flex gap-2 items-center mb-1">
<h2 class="font-bold text-lg">
{{ event.title }}
</h2>
<div v-if="event.wiki">
<Button variant="link" size="xs" as-child class="text-inherit">
<UiButton variant="link" size="xs" as-child class="text-inherit">
<a :href="event.wiki" target="_blank">
Wiki
<PhArrowSquareOut size="16" weight="fill" />
</a>
</Button>
</UiButton>
</div>
</div>
<div class="mb-1">
<div class="flex gap-2 items-center justify-between mb-1">
<template v-if="!event.endDate">
<p class="col-span-2 font-semibold text-sm opacity-75">
<p class="font-semibold text-sm opacity-75">
{{ getFormattedDateTitle(event.startDate, true) }}
</p>
</template>
<template v-else>
<p class="col-span-2 font-semibold text-sm opacity-75">
Du {{ getFormattedDateTitle(event.startDate, true) }} au
{{ getFormattedDateTitle(event.endDate, true) }}
<p class="font-semibold text-sm opacity-75">
{{
$t('entity.calendar.date.fromTo',
{
startDate: getFormattedDateTitle(event.startDate, true),
endDate: getFormattedDateTitle(event.endDate, true)
}
)
}}
</p>
</template>
<div v-if="event.hidden" class="flex justify-end">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiBadge class="flex gap-1">
<PhEye size="16" weight="fill" /> {{ $t('entity.calendar.event.isHidden') }}
</UiBadge>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.event.hiddenTooltip') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
<div class="mb-1 flex gap-x-2 items-center">
<template v-if="event.location">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhMapPinArea size="16" weight="fill" /> {{ event.location }}
</p>
</template>
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p>
<template v-if="dateDuration">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
</p>
</template>
</div>
@@ -91,15 +98,15 @@ const dateDuration: string | null = props.event.endDate
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
<ul class="flex gap-1">
<li v-if="event.category">
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
{{ event.category }}
</Badge>
<UiBadge class="mix-blend-luminosity font-bold bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }}
</UiBadge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat">
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat }}
</Badge>
<li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600 lowercase" variant="secondary">
{{ cat.name }}
</UiBadge>
</li>
</ul>
</div>

View File

@@ -1,39 +1,39 @@
<script lang="ts" setup>
import { isCharacter, type Character } from '@/models/Characters'
import { compareDates, type LeimDate, type LeimDateOrder } from '@/models/Date'
import { isCalendarEvent, type CalendarEvent } from '@/models/Events'
import { useCalendar } from '@/stores/CalendarStore'
import { computed } from 'vue'
import type { SearchMode } from '../../SearchMode'
import { isCharacter, type Character } from "@/models/Characters"
import type { RPGDate, RPGDateOrder } from "@/models/Date"
import { useCalendar } from "@/stores/CalendarStore"
import { computed } from "vue"
import { isCalendarEvent, type CalendarEvent } from "~/models/CalendarEvent"
import type { SearchMode } from "../../SearchMode"
import CharacterCallout from './CharacterCallout.vue'
import EventCallout from './EventCallout.vue'
import CharacterCallout from "./CharacterCallout.vue"
import EventCallout from "./EventCallout.vue"
const props = defineProps<{
results: (Character | CalendarEvent)[]
currentEntity: SearchMode
order: LeimDateOrder
order: RPGDateOrder
startAt: number
endAt: number
limit?: number
}>()
const emit = defineEmits(['jumpedToDate'])
const emit = defineEmits(["jumpedToDate"])
const { jumpToDate } = useCalendar()
const { jumpToDate, compareDates } = useCalendar()
function handleJumpToDate(date?: LeimDate) {
function handleJumpToDate(date?: RPGDate) {
if (!date) return
jumpToDate(date)
emit('jumpedToDate')
emit("jumpedToDate")
}
// Initial sorting of the results
const sortedResults = computed(() => {
return [...props.results].sort((a, b) => {
let firstDate: LeimDate
let secondDate: LeimDate
let firstDate: RPGDate
let secondDate: RPGDate
if (isCalendarEvent(a)) {
firstDate = a.startDate

View File

@@ -1,26 +1,26 @@
<script lang="ts" setup>
import { areDatesIdentical, type LeimDate } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { useElementBounding } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, ref, type ComputedRef } from 'vue'
import type { RPGDate } from "@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent"
import { useElementBounding } from "@vueuse/core"
import { storeToRefs } from "pinia"
import { computed, ref, type ComputedRef } from "vue"
import CalendarEventButton from '../../CalendarEvent.vue'
import type { CalendarEvent } from '@/models/Events'
import CalendarEventButton from "../../CalendarEvent.vue"
const props = defineProps<{
date: LeimDate
date: RPGDate
faded?: boolean
}>()
const calendarTile = ref()
const calendarEventsList = ref()
const { defaultDate, selectDate } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { currentEvents } = storeToRefs(useCalendarEvents())
const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate, currentEvents } = storeToRefs(useCalendar())
/**
* All events with a startDate / endDate that starts or ends on the tile
*/
const eventsForTheDay = computed(() => {
return currentEvents.value.filter((currentEvent) => {
return (
@@ -30,10 +30,16 @@ const eventsForTheDay = computed(() => {
})
})
/**
* Is the tile on today's date ?
*/
const isDefaultDate = computed(() => {
return areDatesIdentical(props.date, defaultDate)
})
/**
* Is the tile on the hightlighted date ?
*/
const isSelectedDate = computed(() => {
return areDatesIdentical(props.date, selectedDate.value)
})
@@ -49,36 +55,40 @@ const numberOfEventsToFit: ComputedRef<number> = computed(() => {
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
})
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed(() => {
/**
* Events that can fit in the tile's space
*/
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(() => {
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
})
const eventsNotDisplayed = computed(
() => eventsForTheDay.value.length - eventsToDisplay.value.length
)
/**
* Remaining events that are not displayed
*
* Used if not all events can be seen in the tile's space
*/
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
</script>
<template>
<div
ref="calendarTile"
class="tile relative text-xs p-2 border-slate-700"
class="tile relative text-xs p-2 border-slate-400 dark:border-slate-700 dark:bg-black transition-colors"
:class="{
'text-slate-500': props.faded,
'text-slate-300': !props.faded
'text-slate-300 dark:text-slate-500': props.faded,
'text-slate-500 dark:text-slate-300': !props.faded
}"
>
<!-- Used for "display all events" -->
<button class="absolute inset-0 w-full h-full cursor-default z-0" />
<button
class="relative z-10 group block w-full text-center cursor-pointer"
@click="selectDate(date)"
>
<ClientOnly>
<span
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-slate-800"
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-indigo-300 dark:group-hover:border-indigo-700"
:class="{
'bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-blue-500': isSelectedDate
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-indigo-500': isSelectedDate
}"
>
{{ date.day }}
@@ -86,6 +96,7 @@ const eventsNotDisplayed = computed(
</ClientOnly>
</button>
<ClientOnly>
<ul
ref="calendarEventsList"
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
@@ -93,9 +104,11 @@ const eventsNotDisplayed = computed(
'opacity-40': props.faded && !isSelectedDate
}"
>
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
<TransitionGroup name="event">
<li v-for="event in eventsToDisplay" :key="event.id" class="grid w-full pointer-events-auto">
<CalendarEventButton :event :tile-date="date" />
</li>
</TransitionGroup>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
<UiPopover>
@@ -127,6 +140,11 @@ const eventsNotDisplayed = computed(
</UiPopover>
</li>
</ul>
</ClientOnly>
<ClientOnly>
<LazyCalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
</ClientOnly>
</div>
</template>
@@ -140,4 +158,23 @@ const eventsNotDisplayed = computed(
border-top-width: 1px;
}
}
.event-enter-active {
transition: all 120ms ease-in-out;
}
.event-move,
.event-leave-active {
position: absolute;
transition: all 200ms ease-in-out;
}
.event-enter-from,
.event-leave-to {
opacity: 0;
}
.event-enter-from {
transform: translateX(.15rem);
}
.event-leave-to {
transform: translateX(-.5rem);
}
</style>

View File

@@ -1,38 +1,9 @@
<script lang="ts" setup>
import type { LeimDate } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { useThrottleFn } from '@vueuse/core'
import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from "@vueuse/core"
import DayTile from './DayTile.vue'
const { staticConfig, currentDate, decrementMonth, incrementMonth } = useCalendar()
const daysPerMonth = staticConfig.daysPerMonth
function getNextMonthDate(day: number): LeimDate {
let nextDay = day
let nextMonth = currentDate.currentMonth + 1
let nextYear = currentDate.currentYear
let nextPeriod = currentDate.currentPeriod
// If the new value would exceed the max number of month per year
if (nextMonth >= staticConfig.monthsPerYear) {
nextMonth = 0
// Increment the year
nextYear++
}
if (nextYear >= 0) {
nextPeriod = 'nante'
}
return {
day: nextDay,
month: nextMonth,
year: nextYear,
period: nextPeriod
}
}
const { currentDate, decrementViewMonth, incrementViewMonth } = useCalendar()
const { currentMonthData } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
const isMovingUp = e.deltaY < 0
@@ -44,30 +15,26 @@ function handleWheel(e: WheelEvent) {
}
const moveCalendarLeft = useThrottleFn(() => {
decrementMonth()
decrementViewMonth()
}, 100)
const moveCalendarRight = useThrottleFn(() => {
incrementMonth()
incrementViewMonth()
}, 100)
</script>
<template>
<div class="grid grid-cols-10" @wheel="handleWheel">
<DayTile
v-for="day in daysPerMonth"
:key="day"
<template v-if="currentMonthData">
<CalendarStateMonthlyDayTile
v-for="day in currentMonthData?.days"
:key="`layout-month-grid-${day}`"
:date="{
day: day,
month: currentDate.currentMonth,
year: currentDate.currentYear
}"
/>
<DayTile
v-for="nextMonthDay in 8"
:key="nextMonthDay"
faded
:date="getNextMonthDate(nextMonthDay)"
/>
</template>
</div>
</template>

View File

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

View File

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

View File

@@ -1,27 +1,21 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import type { CalendarMonth } from "~/models/CalendarMonth";
import DayTile from './DayTile.vue'
const { staticConfig, getMonthName } = useCalendar()
const props = defineProps<{
monthNumber: number
defineProps<{
month: CalendarMonth
}>()
const tileMonthName: string = getMonthName(props.monthNumber)
</script>
<template>
<div>
<div class="font-medium">
{{ tileMonthName }}
{{ month.name }}
</div>
<div class="grid grid-cols-7">
<DayTile
v-for="day in staticConfig.daysPerMonth"
<div class="grid grid-cols-7 gap-1">
<CalendarStateYearlyDayTile
v-for="day in month.days"
:key="day"
:month-number="monthNumber"
:month-number="month.position"
:day-number="day"
/>
</div>

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>
<h1 class="text-2xl font-bold flex">
<h1 v-if="level === 'h1'" class="text-4xl font-bold flex">
<slot />
</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>

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 { PhGlobeHemisphereWest, 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 v-if="user">
<UiTooltipProvider :delay-duration="50">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
<RouterLink to="/my">
<PhGlobeHemisphereWest size="24" weight="fill" />
</RouterLink>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>
{{ $t('entity.world.namePlural') }}
</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>
import { computed } from 'vue'
import { computed } from "vue"
import { PhUserCircle } from '@phosphor-icons/vue'
import { PhCheckCircle, PhGear, PhGlobeHemisphereWest, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
import { cn } from "~/lib/utils";
const router = useRouter()
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const userMeta = computed(() => user.value?.user_metadata)
const profileUrl: string = `${useRequestURL().origin}/my/`
const { locale, setLocale } = useI18n()
const menuOpened = ref<boolean>(false)
@@ -19,12 +23,13 @@ watch(user, closeMenu)
async function handleGoogleLogin() {
try {
auth.signInWithOAuth({
provider: 'google',
provider: "google",
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
access_type: "offline",
prompt: "consent"
},
redirectTo: profileUrl
}
})
} catch (err) {
@@ -42,40 +47,166 @@ async function handleLogout() {
}
}
function gotoProfilePage() {
router.push({ path: '/profile' })
type AvailableRoutes = "/my" | "/my/settings"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script>
<template>
<ClientOnly>
<UiPopover v-model:open="menuOpened">
<UiPopoverTrigger>
<UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger>
<UiAvatar v-if="user" class="cursor-pointer">
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>Me</UiAvatarFallback>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<UiButton v-else variant="outline" size="icon">
<PhUserCircle size="18" />
<UiButton v-else variant="outline" size="icon" class="rounded-full">
<PhUserCircle size="24" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent class="w-fit p-0" :align="'start'" :side="'top'" :collision-padding="20">
<UiCommand>
<UiCommandList v-if="user">
<UiCommandGroup :heading="`Connecté en tant que ${user?.email}`">
<UiCommandItem value="profile" @select="gotoProfilePage"> Profil </UiCommandItem>
<UiCommandItem value="logout" @select="handleLogout"> Déconnexion </UiCommandItem>
</UiCommandGroup>
</UiCommandList>
<UiCommandList v-else>
<UiCommandItem value="logout" @select="handleGoogleLogin"> Connexion </UiCommandItem>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
<template v-if="user">
<p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.greeting', { user: user?.email }) }}
</p>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
<PhGlobeHemisphereWest size="20" weight="fill" />
<span>
{{ $t('entity.world.namePlural') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
</template>
<template v-else>
<p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.anonymousGreeting') }}
</p>
</template>
<UiDropdownMenuSub>
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
<PhPalette size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.appearance') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubTrigger>
<UiDropdownMenuPortal>
<UiDropdownMenuSubContent>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': $colorMode.preference === 'dark' })"
@select.prevent="$colorMode.preference = 'dark'"
>
<PhCheckCircle v-if="$colorMode.preference === 'dark'" size="20" weight="fill" />
<PhMoon v-else size="20" />
<span>
{{ $t('ui.dark') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': $colorMode.preference === 'light' })"
@select.prevent="$colorMode.preference = 'light'"
>
<PhCheckCircle v-if="$colorMode.preference === 'light'" size="20" weight="fill" />
<PhSun v-else size="20" />
<span>
{{ $t('ui.light') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': $colorMode.preference === 'system' })"
@select.prevent="$colorMode.preference = 'system'"
>
<PhCheckCircle v-if="$colorMode.preference === 'system'" size="20" weight="fill" />
<PhLaptop v-else size="20" />
<span>
{{ $t('ui.system') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubContent>
</UiDropdownMenuPortal>
</UiDropdownMenuSub>
<UiDropdownMenuSub>
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
<PhTranslate size="20" />
<span>
{{ $t('ui.sidebarMenu.language') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubTrigger>
<UiDropdownMenuPortal>
<UiDropdownMenuSubContent>
<UiDropdownMenuSubContent>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': locale === 'fr' })"
@select.prevent="setLocale('fr')"
>
<PhCheckCircle v-if="locale === 'fr'" size="20" weight="fill" />
<img v-else src="/images/flag-fr.png" width="20" alt="" loading="eager">
<span>Français</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem
class="flex gap-[.5ch] items-center rounded-none transition-colors"
:class="cn({ 'text-emerald-600': locale === 'en' })"
@select.prevent="setLocale('en')"
>
<PhCheckCircle v-if="locale === 'en'" size="20" weight="fill" />
<img v-else src="/images/flag-uk.png" width="20" alt="" loading="eager">
<span>English</span>
</UiDropdownMenuItem>
</UiDropdownMenuSubContent>
</UiDropdownMenuSubContent>
</UiDropdownMenuPortal>
</UiDropdownMenuSub>
<UiDropdownMenuSeparator />
<template v-if="user">
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my/settings')">
<PhGear size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.account') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="handleLogout">
<PhSignOut size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.logout') }}
</span>
</UiDropdownMenuItem>
</template>
<UiDropdownMenuItem v-if="!user" class="flex gap-[.5ch] items-center rounded-none" @click="handleGoogleLogin">
<PhSignIn size="18" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.login') }}
</span>
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
</ClientOnly>
</template>

View File

@@ -1,9 +0,0 @@
<script lang="ts" setup>
const user = useSupabaseUser()
</script>
<template>
<div class="p-8">
<Heading>{{ user?.user_metadata.full_name }}</Heading>
</div>
</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">
import type { HTMLAttributes } from 'vue'
import { AvatarRoot } from 'radix-vue'
import { type AvatarVariants, avatarVariant } from '.'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from "vue"
import { AvatarRoot } from "radix-vue"
import { type AvatarVariants, avatarVariant } from "."
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
size?: AvatarVariants['size']
shape?: AvatarVariants['shape']
class?: HTMLAttributes["class"]
size?: AvatarVariants["size"]
shape?: AvatarVariants["shape"]
}>(), {
size: 'sm',
shape: 'circle',
size: "sm",
shape: "circle",
})
</script>

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue'
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@/lib/utils'
import { type HTMLAttributes, computed } from "vue"
import type { ComboboxRootEmits, ComboboxRootProps } from "radix-vue"
import { ComboboxRoot, useForwardPropsEmits } from "radix-vue"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes["class"] }>(), {
open: true,
modelValue: ''
modelValue: ""
})
const emits = defineEmits<ComboboxRootEmits>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 CommandDialog } from './CommandDialog.vue'
export { default as CommandEmpty } from './CommandEmpty.vue'
export { default as CommandGroup } from './CommandGroup.vue'
export { default as CommandInput } from './CommandInput.vue'
export { default as CommandItem } from './CommandItem.vue'
export { default as CommandList } from './CommandList.vue'
export { default as CommandSeparator } from './CommandSeparator.vue'
export { default as CommandShortcut } from './CommandShortcut.vue'
export { default as Command } from "./Command.vue"
export { default as CommandDialog } from "./CommandDialog.vue"
export { default as CommandEmpty } from "./CommandEmpty.vue"
export { default as CommandGroup } from "./CommandGroup.vue"
export { default as CommandInput } from "./CommandInput.vue"
export { default as CommandItem } from "./CommandItem.vue"
export { default as CommandList } from "./CommandList.vue"
export { default as CommandSeparator } from "./CommandSeparator.vue"
export { default as CommandShortcut } from "./CommandShortcut.vue"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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