1 Commits

Author SHA1 Message Date
Alexis
4ee412ee0c Added supabase and basic role management 2024-05-06 18:28:13 +02:00
193 changed files with 8173 additions and 11684 deletions

35
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,35 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
overrides: [
{
files: [
'**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/support/**/*.{js,ts,jsx,tsx}'
],
extends: ['plugin:cypress/recommended']
}
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
indent: ['error', 2, { SwitchCase: 1 }],
'prettier/prettier': [
'error',
{
endOfLine: 'auto'
}
],
'vue/multi-word-component-names': 'off'
}
}

44
.gitignore vendored
View File

@@ -1,28 +1,32 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Misc
node_modules
.DS_Store
.fleet
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# Local env files
.env
.env.*
!.env.example
# Supabase
.branches
.temp

3
.npmrc
View File

@@ -1,3 +0,0 @@
# https://pnpm.io/npmrc#shamefully-hoist
# Fixes: WARN Failed to resolve dependency: @supabase/gotrue-js, present in 'optimizeDeps.include'
shamefully-hoist=true

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@@ -1,5 +0,0 @@
{
"eslint.experimental.useFlatConfig": true,
"editor.indentSize": "tabSize",
"editor.tabSize": 2,
}

View File

@@ -1,75 +1,5 @@
# Nuxt 3 Minimal Starter
# Leim Tools
A congregate of tools used to visualize TTRPG elements and help content creators (mostly me for now tbh) build their worlds.
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
## Timeline
Visual helper for time-related data, such as events, eras and characters. Helpful to know how old a character is while something is happening in the world, or how far two events are separated in the timeline.

42
app.vue
View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import { ConfigProvider } from 'radix-vue'
useHead({
titleTemplate: (titleChunk) => {
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'}
],
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' },
]
})
const useIdFunction = () => useId()
</script>
<template>
<div class="h-screen">
<NuxtLoadingIndicator />
<NuxtLayout>
<ConfigProvider :use-id="useIdFunction">
<NuxtPage/>
</ConfigProvider>
</NuxtLayout>
</div>
</template>

View File

@@ -4,11 +4,11 @@
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/main.css",
"css": "src/assets/main.css",
"baseColor": "slate",
"cssVariables": true
},
"framework": "nuxt",
"framework": "vite",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"

View File

@@ -1,38 +0,0 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { areDatesIdentical } from '@/models/Date'
const { defaultDate } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
const isDefaultDate: ComputedRef<boolean> = computed<boolean>(() => areDatesIdentical(selectedDate.value, defaultDate))
</script>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<ClientOnly>
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate">
Aujourd'hui
</UiButton>
<template #fallback>
<UiButton size="sm">
Aujourd'hui
</UiButton>
</template>
</ClientOnly>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</template>

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import { useCalendar } from '@/stores/CalendarStore'
import { PhCalendarBlank } from '@phosphor-icons/vue'
import { computed } from 'vue'
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</script>
<template>
<UiDropdownMenu>
<UiDropdownMenuTrigger as-child>
<UiButton size="sm" variant="secondary">
<PhCalendarBlank size="18" weight="fill" />
{{ viewTypeTitle }}
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
<UiDropdownMenuLabel>Mode d'affichage</UiDropdownMenuLabel>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem
v-for="option in viewTypeOptions"
:key="option"
@click="setViewType(option)"
>
{{ getViewTypeTitle(option) }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<h1 class="text-2xl font-bold flex">
<slot />
</h1>
</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

@@ -1,81 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { PhUserCircle } from '@phosphor-icons/vue'
const router = useRouter()
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const userMeta = computed(() => user.value?.user_metadata)
const menuOpened = ref<boolean>(false)
function closeMenu() {
menuOpened.value = false
}
watch(user, closeMenu)
async function handleGoogleLogin() {
try {
auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
} catch (err) {
console.log(err)
}
}
async function handleLogout() {
try {
const { error } = await auth.signOut()
if (error) throw error
} catch (err) {
console.log(err)
}
}
function gotoProfilePage() {
router.push({ path: '/profile' })
}
</script>
<template>
<ClientOnly>
<UiPopover v-model:open="menuOpened">
<UiPopoverTrigger>
<UiAvatar v-if="user" class="cursor-pointer">
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>Me</UiAvatarFallback>
</UiAvatar>
<UiButton v-else variant="outline" size="icon">
<PhUserCircle size="18" />
</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>
</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

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
interface SkeletonProps {
class?: HTMLAttributes['class']
}
const props = defineProps<SkeletonProps>()
</script>
<template>
<div :class="cn('animate-pulse rounded-md bg-muted', props.class)" />
</template>

View File

@@ -1 +0,0 @@
export { default as Skeleton } from './Skeleton.vue'

15
cypress.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4173'
},
component: {
specPattern: 'src/**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}',
devServer: {
framework: 'vue',
bundler: 'vite'
}
}
})

View File

@@ -0,0 +1,8 @@
// https://on.cypress.io/api
describe('My First Test', () => {
it('visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'You did it!')
})
})

10
cypress/e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["./**/*", "../support/**/*"],
"compilerOptions": {
"isolatedModules": false,
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,39 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
export {}

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@@ -0,0 +1,43 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
// Import global styles
import '@/assets/main.css'
import { mount } from 'cypress/vue'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(MyComponent)

20
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

1
env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -1,15 +0,0 @@
import withNuxt from './.nuxt/eslint.config.mjs'
import stylisticJs from '@stylistic/eslint-plugin-js'
export default withNuxt({
plugins: {
'@stylistic/js': stylisticJs
},
rules: {
'vue/multi-word-component-names': 'off',
'no-unused-vars': 'warn',
'space-in-parens': 'off',
'computed-property-spacing': 'off',
'@stylistic/js/indent': [ 'warn', 2, { SwitchCase: 1 } ],
}
})

32
index.html Normal file
View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Alexis Pelé" />
<meta name="description" content="Outil de visualisation chronologique des évènements de Léim" />
<meta name="generator" content="VueJS" />
<meta name="robots" content="noindex, nofollow">
<title>Outils — TTTools</title>
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#6595b4">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#a9d1eb">
<meta property="og:title" content="Outils — Leim Wiki"/>
<meta property="og:type" content="tabletop-tools" />
<meta property="og:url" content="ttt.alexcreates.fr" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,7 +0,0 @@
export default defineNuxtRouteMiddleware(() => {
const user = useSupabaseUser()
if (!user.value) {
return abortNavigation()
}
})

View File

@@ -1,35 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
'@nuxtjs/supabase',
'@pinia/nuxt',
'@vueuse/nuxt',
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
'@nuxt/eslint',
'shadcn-nuxt'
],
css: ['~/assets/main.css'],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
shadcn: {
prefix: 'Ui',
componentDir: './components/ui'
},
colorMode: {
classSuffix: '',
preference: 'dark',
fallback: 'dark',
},
supabase: {
redirect: false
},
eslint: {}
})

6432
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,57 @@
{
"name": "nuxt-app",
"name": "leim-tool",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:prettier": "prettier . --check"
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
"test:unit": "cypress run --component",
"test:unit:dev": "cypress open --component",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@nuxt/eslint": "^0.3.12",
"@phosphor-icons/vue": "^2.2.1",
"@pinia/nuxt": "^0.5.1",
"@supabase/supabase-js": "^2.43.1",
"@vueuse/core": "^10.9.0",
"@vueuse/nuxt": "^10.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"clsx": "^2.1.0",
"jwt-decode": "^4.0.0",
"lucide-vue-next": "^0.364.0",
"nuxt": "^3.7.0",
"pinia": "^2.1.7",
"radix-vue": "^1.7.4",
"shadcn-nuxt": "^0.10.4",
"tailwind-merge": "^2.3.0",
"radix-vue": "^1.6.1",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.4.27",
"vue-router": "^4.3.2"
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@nuxtjs/color-mode": "^3.4.1",
"@nuxtjs/supabase": "^1.2.2",
"@nuxtjs/tailwindcss": "^6.12.0",
"@stylistic/eslint-plugin-js": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.28",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19",
"cypress": "^13.7.0",
"eslint": "^8.49.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.26.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.17.0",
"npm-run-all2": "^6.1.2",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier": "^3.0.3",
"sass": "^1.72.0",
"start-server-and-test": "^2.0.3",
"tailwindcss": "^3.4.3",
"typescript": "~5.4.0"
"typescript": "~5.4.0",
"vite": "^5.1.6",
"vue-tsc": "^2.0.6"
}
}

View File

@@ -1,30 +0,0 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useCalendar } from '@/stores/CalendarStore'
import type { MenuItem } from '~/components/global/SidebarProps';
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
const { revealAdvancedSearch } = useCalendar()
const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
useHead({
title: 'Calendar'
})
const sidebarMenu: MenuItem[] = [
{
phIcon: PhMagnifyingGlass,
tooltip: 'Recherche avancée',
clickHandler: revealAdvancedSearch
}
]
</script>
<template>
<main class="h-full grid grid-cols-[auto_1fr]">
<Sidebar :menu-items="sidebarMenu" />
<Calendar />
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
</main>
</template>

View File

@@ -1,31 +0,0 @@
<script lang="ts" setup>
import type { MenuItem } from '~/components/global/SidebarProps';
useHead({
title: 'Dashboard'
})
const sidebarMenu: MenuItem[] = []
</script>
<template>
<main class="h-full grid grid-cols-[auto_1fr]">
<Sidebar :menu-items="sidebarMenu" is-home />
<div class="h-full grid place-items-center">
<UiCard class="w-1/3 min-w-72" link="/calendar">
<UiCardHeader>
<UiCardTitle>Calendrier</UiCardTitle>
<UiCardDescription>
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
chronologie
</UiBadge>
</UiCardDescription>
</UiCardHeader>
<UiCardContent>
Chronologie complète de Léïm, rassemblant les évènements et personnages clés du monde
</UiCardContent>
</UiCard>
</div>
</main>
</template>

View File

@@ -1,29 +0,0 @@
<script lang="ts" setup>
import type { MenuItem } from '~/components/global/SidebarProps';
const user = useSupabaseUser()
useHead({
title: 'Profil'
})
definePageMeta({
middleware: ['auth-guard']
})
// Redirect user back home when they log out on the page
watch(user, (n, _o) => {
if (!n) {
navigateTo('/')
}
})
const sidebarMenu: MenuItem[] = []
</script>
<template>
<main class="h-full grid grid-cols-[auto_1fr]">
<Sidebar :menu-items="sidebarMenu" />
<ProfileLayout />
</main>
</template>

10204
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,252 +0,0 @@
export default defineEventHandler(() => {
return [
/**
* Player characters
*/
// "Les Milles Cages"
{
name: 'Sulvan Trois-Barbes',
birth: { day: 20, month: 3, year: 3169 },
wiki: 'https://alexcreates.fr/leim/index.php/Sulvan_Trois-Barbes',
category: 'joueur',
secondaryCategories: ['criminel', 'étincelle']
},
{
name: 'Vascylly',
birth: { day: 3, month: 5, year: 3181 },
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly',
category: 'joueur',
secondaryCategories: ['mage']
},
{
name: 'Tara Belyus',
birth: { day: 14, month: 9, year: 3186 },
category: 'joueur',
secondaryCategories: ['mage']
},
// "Les Cloches de Cantane"
{
name: 'Malik',
birth: { day: 22, month: 8, year: 3181 },
category: 'joueur',
secondaryCategories: ['scientifique']
},
{
name: 'Elie',
birth: { day: 5, month: 6, year: 3192 },
category: 'joueur',
secondaryCategories: ['ecclésiastique']
},
{
name: 'Clayron',
birth: { day: 3, month: 5, year: 3178 },
category: 'joueur',
secondaryCategories: ['sentinelle']
},
/**
* NPC
*/
// Counts of the Alliance
{
name: 'Alfrid de Lagarde',
birth: { day: 29, month: 7, year: 3193 },
wiki: 'https://alexcreates.fr/leim/index.php/Alfrid_de_Lagarde',
category: 'comte',
secondaryCategories: ['sentinelle']
},
{
name: 'Alaric de Lagarde',
birth: { day: 23, month: 8, year: 3192 },
wiki: 'https://alexcreates.fr/leim/index.php/Alaric_de_Lagarde',
category: 'comte',
secondaryCategories: ['sentinelle']
},
{
name: 'Anastael III de Quillon',
birth: { day: 1, month: 5, year: 3184 },
wiki: 'https://alexcreates.fr/leim/index.php/Anastael_III_de_Quillon',
category: 'comte',
secondaryCategories: ['sentinelle', 'buse blanche']
},
{
name: 'Antoine de Mireloin',
birth: { day: 31, month: 5, year: 3190 },
wiki: 'https://alexcreates.fr/leim/index.php/Antoine_de_Mireloin',
category: 'comte'
},
{
name: 'Armance Ronchère',
birth: { day: 1, month: 1, year: 3132 },
wiki: 'https://alexcreates.fr/leim/index.php/Armance_Ronchère',
category: 'comte'
},
{
name: 'Baranne Rougefer',
birth: { day: 1, month: 0, year: 3175 },
wiki: 'https://alexcreates.fr/leim/index.php/Baranne_Rougefer',
category: 'comte',
secondaryCategories: ['sentinelle']
},
{
name: 'Béatrice II de Grandlac',
birth: { day: 21, month: 4, year: 3158 },
wiki: 'https://alexcreates.fr/leim/index.php/Béatrice_II_de_Grandlac',
category: 'comte',
secondaryCategories: ['mage']
},
{
name: 'Favio Asharos-Losantelle',
birth: { day: 17, month: 3, year: 3091 },
wiki: 'https://alexcreates.fr/leim/index.php/Favio_Asharos-Losantelle',
category: 'comte',
secondaryCategories: ['mage']
},
{
name: 'Firmin de Montardieu',
birth: { day: 9, month: 2, year: 3203 },
wiki: 'https://alexcreates.fr/leim/index.php/Firmin_de_Montardieu',
category: 'comte'
},
{
name: 'Laura de Montardieu',
birth: { day: 32, month: 3, year: 3167 },
death: { day: 1, month: 4, year: 3217 },
hiddenDeath: true,
wiki: 'https://alexcreates.fr/leim/index.php/Laura_de_Montardieu',
category: 'comte'
},
{
name: 'Lazarus Tymos',
birth: { day: 29, month: 9, year: 3145 },
wiki: 'https://alexcreates.fr/leim/index.php/Lazarus_Tymos',
category: 'comte',
secondaryCategories: ['mage', 'buse blanche']
},
{
name: 'Marion de Corambre',
birth: { day: 14, month: 7, year: 3190 },
wiki: 'https://alexcreates.fr/leim/index.php/Marion_de_Corambre',
category: 'comte',
secondaryCategories: ['scientifique', 'ecclésiastique']
},
{
name: 'Relforg Pergaré',
birth: { day: 18, month: 9, year: 3182 },
wiki: 'https://alexcreates.fr/leim/index.php/Relforg_Pergaré',
category: 'comte'
},
{
name: 'Vilgarde de Ternâcre',
birth: { day: 3, month: 3, year: 2998 },
wiki: 'https://alexcreates.fr/leim/index.php/Vilgarde_de_Ternâcre',
category: 'comte'
},
{
name: 'Ysildy Milopée',
birth: { day: 3, month: 1, year: 3187 },
wiki: 'https://alexcreates.fr/leim/index.php/Ysildy_Milopée',
category: 'comte',
secondaryCategories: ['mage', 'scientifique', 'professeur']
},
// Sparks
{
name: 'Izàc Tymos',
birth: { day: 13, month: 6, year: 3192 },
wiki: 'https://alexcreates.fr/leim/index.php/Izàc_Tymos',
category: 'mage',
secondaryCategories: ['étincelle', 'criminel', 'professeur', 'scientifique']
},
{
name: 'Tvernée',
birth: { day: 19, month: 2, year: 3205 },
wiki: 'https://alexcreates.fr/leim/index.php/Tvernée',
category: 'étincelle',
secondaryCategories: ['criminel']
},
// Pirates
{
name: 'Räzal',
birth: { day: 13, month: 8, year: 3178 },
wiki: 'https://alexcreates.fr/leim/index.php/Räzal'
},
// Legends
{
name: 'Jorhas Kirendre',
birth: { day: 2, month: 9, year: -452 },
death: { day: 2, month: 2, year: -419 },
category: 'sentinelle',
wiki: 'https://alexcreates.fr/leim/index.php/Jorhas_Kirendre'
},
// "Les Milles Cages"
{
name: 'Ernestin Pomel',
birth: { day: 11, month: 2, year: 3179 },
wiki: 'https://alexcreates.fr/leim/index.php/Ernestin_Pomel',
category: 'mage'
},
{
name: 'Quacille Lévios',
birth: { day: 3, month: 6, year: 3162 },
wiki: 'https://alexcreates.fr/leim/index.php/Quacille_Lévios',
category: 'professeur',
secondaryCategories: ['mage']
},
{
name: 'Morel Lévios',
birth: { day: 26, month: 3, year: 3157 },
wiki: 'https://alexcreates.fr/leim/index.php/Morel_Lévios',
category: 'sentinelle',
secondaryCategories: ['mage', 'buse blanche']
},
{
name: 'Anastael Simon',
birth: { day: 32, month: 2, year: 3166 },
wiki: 'https://alexcreates.fr/leim/index.php/Anastael_Simon',
category: 'étincelle',
secondaryCategories: ['mage']
},
{
name: 'Grestain',
birth: { day: 9, month: 2, year: 3162 },
wiki: 'https://alexcreates.fr/leim/index.php/Grestain',
category: 'criminel',
secondaryCategories: ['mage']
},
{
name: 'Lucien Malanth',
birth: { day: 31, month: 4, year: 3167 },
wiki: 'https://alexcreates.fr/leim/index.php/Lucien_Malanth',
category: 'criminel',
secondaryCategories: ['mage']
},
{
name: 'Tivian Rodhus',
birth: { day: 13, month: 3, year: 3157 },
wiki: 'https://alexcreates.fr/leim/index.php/Tivian_Rodhus',
category: 'professeur',
secondaryCategories: ['mage', 'criminel']
},
// "Les Cloches de Cantane"
{
name: 'Bénédicte Vaht',
description: "Moine d'Ikami ayant entrepris la construction du Pilier Blanc de Cantane",
birth: { day: 28, month: 6, year: 3139 },
category: 'ecclésiastique'
},
{
name: 'Taleb Vaht',
description:
"Fils de Bénédicte, il s'est engagé auprès des Étincelles comme alchimiste artificier.",
birth: { day: 9, month: 1, year: 3180 },
death: { day: 28, month: 7, year: 3209 },
category: 'étincelle',
secondaryCategories: ['scientifique']
}
]
})

View File

@@ -1,217 +0,0 @@
export default defineEventHandler(() => {
return [
// Histoire Antique
{
title: "Laurdieu devient la première cité de l'empire de Kaliatos",
startDate: { day: 3, month: 4, year: -1932 },
description:
"L'empire de Kaliatos établi sa capitale dans la cité de Laurdieu, qui connaitra une prospérité nouvelle au sein d'Aldys.",
category: 'législation',
secondaryCategories: ['inauguration'],
wiki: 'https://alexcreates.fr/leim/index.php/Laurdieu',
hidden: true
},
{
title: "Apparition d'Asménys",
startDate: { day: 19, month: 7, year: -1358 },
description:
"La défunte chanteuse Asménys apparaît à un barde pendant son jeune public, démarrant la religion des Prêtresses d'Asménys.",
category: 'religion',
secondaryCategories: ['bénédiction'],
wiki: 'https://alexcreates.fr/leim/index.php/Pr%C3%AAtresses_d%27Asm%C3%A9nys',
hidden: true
},
{
title: 'La Rupture',
startDate: { day: 26, month: 5, year: -756 },
endDate: { day: 4, month: 9, year: 29 },
description:
"Les Abysses se déversent à la surface de Léim, à travers plusieurs brèches. Plusieurs hordes de démons se rapprochent des villes, et ce qu'on appellera l'Âge des Abysses commencent alors sur la planète entière.",
category: 'catastrophe',
secondaryCategories: ['catastrophe naturelle'],
wiki: 'https://alexcreates.fr/leim/index.php/Seconde_Rupture',
hidden: true
},
{
title: 'Marche du sang',
startDate: { day: 18, month: 9, year: -420 },
endDate: { day: 27, month: 1, year: -419 },
description:
"L'empereur de Kaliatos ordonne personnellement la traque de Jorhas Kirendre pour connivence avec les démons. Plusieurs bataillons sont affectés à la chasse de Jorhas, qui se terminera par son incarcération ainsi que la mort de plusieurs centaines de soldats.",
category: 'criminalité',
wiki: 'https://alexcreates.fr/leim/index.php/Jorhas_Kirendre',
hidden: true
},
{
title: 'Exécution de Tyhos',
startDate: { day: 1, month: 0, year: 0 },
description:
"Le léviathan Tyhos rend l'âme après un combat de plusieurs années contre Lystos, le dieu du Soleil.",
category: 'bénédiction',
secondaryCategories: ['mort'],
wiki: 'https://alexcreates.fr/leim/index.php/Tyhos',
hidden: true
},
{
title: 'Traité de Kadel',
startDate: { day: 29, month: 4, year: 100 },
description: '',
category: 'inauguration',
secondaryCategories: ['législation'],
wiki: 'https://alexcreates.fr/leim/index.php/Alliance_Kald%C3%A9lienne#Trait%C3%A9_de_Kadel'
},
{
title: 'Découverte des Plaines de Poussières',
startDate: { day: 17, month: 7, year: 305 },
description:
"Les troupes de la reconquête aldienne explorent le littoral d'une immense étendue grise et inerte.",
category: 'découverte',
secondaryCategories: ['exploration'],
wiki: 'https://alexcreates.fr/leim/index.php/Plaines_de_poussi%C3%A8re',
hidden: true
},
{
title: 'Construction du Rempart de Laurdieu',
startDate: { day: 30, month: 2, year: 340 },
endDate: { day: 27, month: 9, year: 355 },
description:
"Le Grand Conseil Kaldélien ordonne la construction d'une muraille autour des Plaines de Poussières, afin de contenir les démons y sortant.",
category: 'construction',
wiki: 'https://alexcreates.fr/leim/index.php/Plaines_de_poussi%C3%A8re',
hidden: true
},
// Histoire Récente
{
title: "Inauguration de l'Académie Artistique Arcanique",
startDate: { day: 11, month: 6, year: 2545 },
description:
"Scäd Sceni ouvre son institut artistique dédié à l'apprentissage des arts arcaniques",
category: 'inauguration',
secondaryCategories: ['arcanologie'],
wiki: 'https://alexcreates.fr/leim/index.php/Buse_(arme)'
},
{
title: 'Création de la buse kaldélienne',
startDate: { day: 4, month: 2, year: 3113 },
description:
'Sophia de Rougefer invente la buse kaldélienne, une arme à feu souple à deux canons.',
category: 'invention',
secondaryCategories: ['science'],
wiki: 'https://alexcreates.fr/leim/index.php/Buse_(arme)'
},
{
title: 'Feux dans les champs de Bamast',
startDate: { day: 9, month: 5, year: 3200 },
description:
"Plusieurs incendies criminels se propagent à travers les champs de sérille des fermiers des littoraux de Bamast. Aucun suspect ni coupable ne sera trouvé et l'enquête sera baclée.",
category: 'catastrophe',
secondaryCategories: ['criminalité'],
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly'
},
{
title: 'Meurtre de Darléon Typhos',
startDate: { day: 21, month: 6, year: 3200 },
description:
'Darléon Typhos surprend Vascylly fouillant sa demeure peu après la tombée de la nuit. Le majordome de la famille Typhos découvre le corps sans vie de son maître le lendemain.',
category: 'criminalité',
secondaryCategories: ['joueurs'],
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly'
},
{
title: 'Scandale Rodhus',
startDate: { day: 25, month: 9, year: 3208 },
description:
"Tivian Rodhus, un professeur estimé de l'Académie Centrale Kaldélienne, est emprisonné pour corruption et aggression sexuelle. Le corps enseignant y est remanié sur ordre de Lazarus Tymos, comte de Nacride.",
category: 'criminalité',
secondaryCategories: ['scandale'],
wiki: 'https://alexcreates.fr/leim/index.php/Tivian_Rodhus'
},
// "Les Milles Cages"
{
title: "Arrivée d'aventuriers à Borélis",
startDate: { day: 12, month: 7, year: 3209 },
description:
'Tara Belyus, Vascylly et Adol Sulvan livrent 3 condamnés à Handany. Ils partent pour la mer durant la journée.',
category: 'joueurs'
},
{
title: "Naufrage de l'Éclipse",
description:
"L'Éclipse, le navire de la garde contenant des condamnés à destination des Cages Handaniennes, s'échoue au large des côtes montagneuses de la Lance d'Aldys.",
startDate: { day: 14, month: 7, year: 3209 },
category: 'catastrophe'
},
{
title: 'Emprisonnement de Tivian Rodhus',
description: "Celui qu'on surnomme la Bête d'Ambrose arrive à Handany, où il purgera sa peine.",
startDate: { day: 14, month: 7, year: 3209 },
category: 'législation'
},
{
title: 'Sulvan et Anastael atteignent Bamast',
startDate: { day: 19, month: 2, year: 3210 },
category: 'joueurs'
},
{
title: 'Jugement de Bormis Griloup',
description:
"Bromis Griloup est jugé coupable d'escroquerie et sabotage aux Cours d'Acier de Tourgrise. Il purgera une peine de 10 ans au sein des prisons royales.",
startDate: { day: 4, month: 8, year: 3209 },
category: 'législation'
},
// "Les Cloches de Cantane"
{
title: 'Inauguration de la Cloche du Pilier',
description: "Le Moine Premier inaugure la grande cloche d'argent au sommet du Pilier d'Ikami.",
startDate: { day: 29, month: 5, year: 3209 },
category: 'religion',
secondaryCategories: ['inauguration']
},
{
title: '1ère disparation à Cantane',
description: "Taleb Vaht décède dans une grotte à la suite d'une attaque d'ischiels enragées.",
startDate: { day: 28, month: 7, year: 3209 },
category: 'mort',
hidden: true
},
{
title: '2ème disparation à Cantane',
description: 'Donovane le mineur kéturien disparait sans laisser de traces.',
startDate: { day: 32, month: 7, year: 3209 },
category: 'mort',
hidden: true
},
{
title: '3ème disparation à Cantane',
description: 'Disparition de Sébastien, gredin sauride.',
startDate: { day: 10, month: 8, year: 3209 },
category: 'mort',
hidden: true
},
{
title: '4ème disparation à Cantane',
description: 'Disparition de Thérence, patrouilleur sauride de la Vieille Garde.',
startDate: { day: 19, month: 8, year: 3209 },
category: 'mort',
hidden: true
},
{
title: '5ème disparation à Cantane',
description: 'Disparition de Mathilda Boulais, vendeuse de pierres.',
startDate: { day: 22, month: 8, year: 3209 },
category: 'mort',
hidden: true
},
{
title: 'Grande Banque Minérale de Cantane',
description:
'Les artisans et mineurs de Rougefer se réunissent à Cantane pour vendre le fruit de leur dur labeur.',
startDate: { day: 23, month: 8, year: 3209 },
endDate: { day: 26, month: 8, year: 3209 },
category: 'commerce'
}
]
})

View File

@@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

14
src/App.vue Normal file
View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { useDark } from '@vueuse/core'
import { RouterView } from 'vue-router'
import { useAuth } from './stores/AuthStore'
useDark()
useAuth()
</script>
<template>
<div class="h-screen">
<RouterView />
</div>
</template>

View File

@@ -2,7 +2,8 @@
@tailwind components;
@tailwind utilities;
:root {
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
@@ -35,7 +36,7 @@
--radius: 0.5rem;
}
:root.dark {
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@@ -64,6 +65,7 @@
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { supabase } from '@/supabase/client'
import Button from '../ui/button/Button.vue'
async function handleGoogleLogin() {
supabase.auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
}
</script>
<template>
<Button @click="handleGoogleLogin"> Google </Button>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { useAuth } from '@/stores/AuthStore'
const { sessionToken } = useAuth()
</script>
<template>
<pre class="text-[.6em]">
{{ sessionToken }}
</pre>
</template>

View File

@@ -10,10 +10,6 @@ import DecadeLayout from './state/decennially/Layout.vue'
import YearLayout from './state/yearly/Layout.vue'
const { currentConfig } = useCalendar()
const { selectedDate, jumpToDate } = useCalendar()
const { fetchEvents } = useCalendarEvents()
fetchEvents()
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) {
@@ -31,16 +27,11 @@ const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
return CenturyLayout
}
})
onMounted(() => {
jumpToDate(selectedDate)
})
</script>
<template>
<div class="h-full grid grid-rows-[auto,1fr]">
<CalendarMenu />
<KeepAlive>
<component :is="currentViewComponent" />
</KeepAlive>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { getRelativeString } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
@@ -14,19 +15,10 @@ const dateDifference = computed(() => getRelativeString(defaultDate, selectedDat
</script>
<template>
<ClientOnly>
<h1 class="text-2xl font-bold flex items-center gap-1">
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
</h1>
<h2 class="text-lg italic opacity-75">
{{ dateDifference }}
</h2>
<template #fallback>
<div class="grid gap-1">
<UiSkeleton class="h-8 w-64" />
<UiSkeleton class="h-6 w-28" />
</div>
</template>
</ClientOnly>
<h1 class="text-2xl font-bold flex items-center gap-1">
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
</h1>
<h2 class="text-lg italic opacity-75">
{{ dateDifference }}
</h2>
</template>

View File

@@ -1,10 +1,11 @@
<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 { ref } from 'vue'
import CalendarEventDetails from './CalendarEventDetails.vue'
import { Popover, PopoverTrigger } from '@/components/ui/popover'
const props = defineProps<{
event: CalendarEvent
@@ -25,8 +26,8 @@ function handleClosePopover() {
</script>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopoverTrigger as-child>
<Popover v-model:open="isPopoverOpen">
<PopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
:class="
@@ -57,15 +58,15 @@ function handleClosePopover() {
>
{{ event.title }}
</button>
</UiPopoverTrigger>
</PopoverTrigger>
<CalendarEventDetails
v-once
:event
:spans-multiple-days
:is-start-event
:is-end-event
@query:close-popover="handleClosePopover"
v-once
/>
</UiPopover>
</Popover>
</template>

View File

@@ -12,6 +12,8 @@ import {
PhArrowBendDoubleUpLeft,
PhArrowBendDoubleUpRight
} from '@phosphor-icons/vue'
import { Badge } from '@/components/ui/badge'
import { PopoverContent } from '@/components/ui/popover'
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
const { getRelativeEventFromEvent } = useCalendarEvents()
@@ -49,8 +51,8 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
</script>
<template>
<UiPopoverContent
class="event-details w-96 bg-slate-900"
<PopoverContent
class="event-details w-96"
:align="'center'"
:align-offset="50"
:side="'left'"
@@ -109,21 +111,21 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
<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">
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
{{ event.category }}
</UiBadge>
</Badge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat">
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat }}
</UiBadge>
</Badge>
</li>
</ul>
</template>
<template v-if="event.description">
<hr class="border-slate-500 mt-2" >
<hr class="border-slate-500 mt-2" />
<div class="mt-2 text-sm text-slate-300">
{{ event.description }}
@@ -132,47 +134,47 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
</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>
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child>
<button
class="flex gap-1"
title="Naviguer au début"
@click="handleJumpToDate(event.startDate!)"
title="Naviguer au début"
>
<PhHourglassHigh size="16" weight="fill" /> Début
</button>
</UiBadge>
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
</Badge>
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
<button
class="flex gap-1"
title="Naviguer à la fin"
@click="handleJumpToDate(event.endDate!)"
title="Naviguer à la fin"
>
<PhHourglassLow size="16" weight="fill" /> Fin
</button>
</UiBadge>
</Badge>
</nav>
<nav class="mt-2 flex gap-2">
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child>
<button
class="flex gap-1"
title="Évènement précédent"
@click="handleGotoRelativeEvent('prev')"
title="Évènement précédent"
>
<PhArrowBendDoubleUpLeft size="16" weight="fill" /> Précédent
</button>
</UiBadge>
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
</Badge>
<Badge 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')"
title="Évènement suivant"
>
<PhArrowBendDoubleUpRight size="16" weight="fill" /> Suivant
</button>
</UiBadge>
</Badge>
</nav>
</UiPopoverContent>
</PopoverContent>
</template>
<style lang="scss">

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { Button } from '@/components/ui/button'
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import CalendarMenuNav from './CalendarMenuNav.vue'
import CalendarMenuSubnav from './CalendarMenuSubnav.vue'
@@ -28,10 +29,10 @@ const { revealAdvancedSearch } = useCalendar()
<menu class="flex items-center gap-2">
<li>
<UiButton search-slash @click="revealAdvancedSearch()">
<Button search-slash @click="revealAdvancedSearch()">
<PhMagnifyingGlass size="20" weight="light" />
Recherche avancée
</UiButton>
</Button>
</li>
<li>
<CalendarSwitch />

View File

@@ -2,12 +2,14 @@
import { computed, type ComputedRef } from 'vue'
import { useCalendar } from '@/stores/CalendarStore'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import {
PhCaretDoubleLeft,
PhCaretDoubleRight,
PhCaretLeft,
PhCaretRight
} from '@phosphor-icons/vue'
import Button from '@/components/ui/button/Button.vue'
interface DirectionLabels {
pastFar: string
@@ -143,56 +145,56 @@ function toFutureFar(): void {
<template>
<div class="flex items-center gap-2">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toPastFar()">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toPastFar()">
<PhCaretDoubleLeft size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ activeDirectionLabels.pastFar }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toPastNear()">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toPastNear()">
<PhCaretLeft size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ activeDirectionLabels.pastNear }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toFutureNear()">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toFutureNear()">
<PhCaretRight size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ activeDirectionLabels.futureNear }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toFutureFar()">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toFutureFar()">
<PhCaretDoubleRight size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ activeDirectionLabels.futureFar }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</template>

View File

@@ -3,7 +3,9 @@ import { daysPerMonth, monthsPerYear, type LeimDate } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue'
import Button from '@/components/ui/button/Button.vue'
const { currentDate, currentConfig, jumpToDate } = useCalendar()
const { getRelativeEventFromDate } = useCalendarEvents()
@@ -60,54 +62,46 @@ 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">
<ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span>
<template #fallback>
<span class="inline-block">
<UiSkeleton class="h-[19px] w-full rounded-sm" />
</span>
</template>
</ClientOnly>
<div class="w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<span class="text-sm">{{ currentDate.currentDateTitle }}</span>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="rounded-t-sm rounded-b-none"
@click="handleGotoPreviousEventPage('prev')"
>
<PhArrowLineLeft size="22" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Précédente page à évènements</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="rounded-t-sm rounded-b-none"
@click="handleGotoPreviousEventPage('next')"
>
<PhArrowLineRight size="22" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Prochaine page à évènements</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useCalendar } from '@/stores/CalendarStore'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import Button from '../ui/button/Button.vue'
import { areDatesIdentical } from '@/models/Date'
const { defaultDate, selectedDate } = storeToRefs(useCalendar())
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
const defaultDateFormatted = getFormattedDateTitle(defaultDate.value, true)
const isDefaultDate = computed(() => {
return areDatesIdentical(selectedDate.value, defaultDate.value)
})
</script>
<template>
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button @click="jumpToDefaultDate" size="sm" :disabled="isDefaultDate">
Aujourd'hui
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ defaultDateFormatted }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { useCalendar } from '@/stores/CalendarStore'
import { PhCalendarBlank } from '@phosphor-icons/vue'
import { computed } from 'vue'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import Button from '../ui/button/Button.vue'
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button size="sm" variant="secondary">
<PhCalendarBlank size="18" weight="fill" />
{{ viewTypeTitle }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent :side="'bottom'" :collision-padding="30">
<DropdownMenuLabel>Mode d'affichage</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
v-for="option in viewTypeOptions"
:key="option"
@click="setViewType(option)"
>
{{ getViewTypeTitle(option) }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import Button from '../ui/button/Button.vue'
import UserCTA from '../global/user/CTA.vue'
const { revealAdvancedSearch } = useCalendar()
</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">
<Button variant="ghost" size="icon" @click="console.log" class="rounded-full">
<PhList size="27" />
</Button>
</li>
<li>
<TooltipProvider :delayDuration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="rounded-full" as-child>
<RouterLink to="/">
<PhHouse size="24" weight="fill" />
</RouterLink>
</Button>
</TooltipTrigger>
<TooltipContent :side="'right'">
<p>Retourner aux outils</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</li>
<li>
<TooltipProvider :delayDuration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="rounded-full"
@click="revealAdvancedSearch()"
>
<PhMagnifyingGlass size="24" weight="fill" />
</Button>
</TooltipTrigger>
<TooltipContent :side="'right'">
<p>Recherche avancée</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</li>
</menu>
<div>
<UserCTA />
</div>
</nav>
</template>

View File

@@ -17,8 +17,31 @@ 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'
import { searchUnifier, type SearchMode } from '../Search'
import { Button } from '@/components/ui/button'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import {
Pagination,
PaginationEllipsis,
PaginationFirst,
PaginationLast,
PaginationList,
PaginationListItem,
PaginationNext,
PaginationPrev
} from '@/components/ui/pagination'
import {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
} from '@/components/ui/tags-input'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
import {
ComboboxAnchor,
@@ -30,10 +53,10 @@ import {
import SearchList from './lists/SearchList.vue'
const { characters } = storeToRefs(useCharacters())
const { allEvents } = storeToRefs(useCalendarEvents())
const { characters } = useCharacters()
const { allEvents } = useCalendarEvents()
const modalOpen = defineModel<boolean>({ default: false })
const modalOpen = defineModel({ default: false })
const searchQuery = ref<string>('')
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
@@ -73,11 +96,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Assign data to loop over and filter
// They are assigned this way for readability
if (selectedEntity.value === 'events') {
dataToFilter = allEvents.value
dataToFilter = allEvents
} else if (selectedEntity.value === 'characters') {
dataToFilter = characters.value
dataToFilter = characters
} else {
dataToFilter = [...allEvents.value, ...characters.value]
dataToFilter = [...allEvents, ...characters]
}
/**
@@ -105,7 +128,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
const allCategories: CalendarEventCategory[] = []
let allCategories: CalendarEventCategory[] = []
if (item.category) {
allCategories.push(item.category)
@@ -139,7 +162,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
const allCategories: CharacterCategory[] = []
let allCategories: CharacterCategory[] = []
if (item.category) {
allCategories.push(item.category)
@@ -170,16 +193,16 @@ function resetSearch() {
}
/**
* Opens the search Uidialog
* Opens the search dialog
*/
function openUiDialog() {
function openDialog() {
modalOpen.value = true
}
/**
* Closes the search Uidialog
* Closes the search dialog
*/
function closeUiDialog() {
function closeDialog() {
modalOpen.value = false
}
@@ -195,13 +218,13 @@ function handleEntitySwitch() {
const keys = useMagicKeys()
whenever(keys.control_period, () => {
openUiDialog()
openDialog()
})
const searchResultsRef = ref<HTMLElement | null>(null)
const { y: searchResultsY } = useScroll(searchResultsRef)
watch([currentPage, selectedEntity], () => {
watch(currentPage, () => {
searchResultsY.value = 0
})
@@ -227,10 +250,10 @@ const filteredCategories = computed(() =>
*
* @param e Radix Change Event
*/
function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
if (typeof e === 'string') {
function handleCategorySelect(e: any) {
if (typeof e.detail.value === 'string') {
searchCategory.value = ''
selectedCategories.value.push(e)
selectedCategories.value.push(e.detail.value)
}
if (filteredCategories.value.length === 0) {
@@ -240,31 +263,31 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
</script>
<template>
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()">
<UiDialogContent
<Dialog v-model:open="modalOpen" @update:open="resetSearch()">
<DialogContent
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="{
'bottom-16': searchResults.length > 0
}"
>
<VisuallyHidden>
<UiDialogTitle> Recherche avancée </UiDialogTitle>
<DialogTitle> Recherche avancée </DialogTitle>
</VisuallyHidden>
<VisuallyHidden>
<UiDialogDescription>
<DialogDescription>
Rechercher les données disponibles sur le calendrier
</UiDialogDescription>
</DialogDescription>
</VisuallyHidden>
<!-- UiDialog header -->
<div id="searchForm" class="grid gap-3">
<!-- Dialog header -->
<div class="grid gap-3" id="searchForm">
<div class="relative w-full h-fit">
<UiInput
<Input
id="search"
v-model:model-value="searchQuery"
type="text"
placeholder="Rechercher le calendrier"
class="pl-10 py-6 text-lg"
v-model:model-value="searchQuery"
autocomplete="off"
/>
<span class="absolute start-1 inset-y-0 flex items-center justify-center px-2 opacity-50">
@@ -274,28 +297,28 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
<div class="flex items-center justify-between">
<div>
<UiToggleGroup
v-model="selectedEntity"
<ToggleGroup
type="single"
class="justify-start"
v-model="selectedEntity"
@update:model-value="handleEntitySwitch()"
>
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
<ToggleGroupItem value="events" aria-label="Uniquement les évènements" v-once>
Évènements
</UiToggleGroupItem>
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
</ToggleGroupItem>
<ToggleGroupItem value="characters" aria-label="Uniquement les personnages" v-once>
Personnages
</UiToggleGroupItem>
</UiToggleGroup>
</ToggleGroupItem>
</ToggleGroup>
</div>
<div class="flex items-center gap-1">
<UiTagsInput class="px-0 gap-0 w-72" :model-value="selectedCategories">
<TagsInput 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 />
</UiTagsInputItem>
<TagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
<TagsInputItemText class="capitalize" />
<TagsInputItemDelete />
</TagsInputItem>
</div>
<ComboboxRoot
@@ -306,7 +329,7 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
>
<ComboboxAnchor as-child>
<ComboboxInput placeholder="Catégories" as-child>
<UiTagsInputInput
<TagsInputInput
class="w-full px-3"
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
@keydown.enter.prevent
@@ -315,79 +338,79 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
</ComboboxAnchor>
<ComboboxPortal :to="'#searchForm'">
<UiCommandList
<CommandList
position="popper"
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
:dismissable="true"
>
<UiCommandEmpty />
<UiCommandGroup>
<UiCommandItem
v-for="cat in filteredCategories"
:key="cat"
:value="cat"
@select.prevent="handleCategorySelect(cat)"
<CommandEmpty />
<CommandGroup>
<CommandItem
v-for="framework in filteredCategories"
:key="framework"
:value="framework"
@select.prevent="handleCategorySelect"
>
{{ capitalize(cat) }}
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
{{ capitalize(framework) }}
</CommandItem>
</CommandGroup>
</CommandList>
</ComboboxPortal>
</ComboboxRoot>
</UiTagsInput>
</TagsInput>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
size="icon"
@click="setOrderDesc()"
>
<PhClockCounterClockwise size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Plus ancien</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
size="icon"
@click="setOrderAsc()"
>
<PhClockClockwise size="18" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Plus récent</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
<hr >
<hr />
<div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto">
<div v-if="searchResults.length > 0" class="grow overflow-y-auto" ref="searchResultsRef">
<SearchList
:results="searchResults"
:current-entity="selectedEntity"
:order="selectedOrder"
:start-at="startOfList"
:end-at="endOfList"
@jumped-to-date="closeUiDialog()"
@jumped-to-date="closeDialog()"
/>
</div>
<div class="flex justify-end">
<UiPagination
<Pagination
v-model:page="currentPage"
:total="searchResults.length"
:items-per-page="itemsPerPage"
@@ -395,32 +418,32 @@ function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
show-edges
:default-page="1"
>
<UiPaginationList v-slot="{ items }" class="flex items-center gap-1">
<UiPaginationFirst />
<UiPaginationPrev />
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
<PaginationFirst />
<PaginationPrev />
<template v-for="(item, index) in items">
<UiPaginationListItem
<PaginationListItem
v-if="item.type === 'page'"
:key="index"
:value="item.value"
as-child
>
<UiButton
<Button
class="w-10 h-10 p-0"
:variant="item.value === currentPage ? 'default' : 'outline'"
>
{{ item.value }}
</UiButton>
</UiPaginationListItem>
<UiPaginationEllipsis v-else :key="item.type" :index="index" />
</Button>
</PaginationListItem>
<PaginationEllipsis v-else :key="item.type" :index="index" />
</template>
<UiPaginationNext />
<UiPaginationLast />
</UiPaginationList>
</UiPagination>
<PaginationNext />
<PaginationLast />
</PaginationList>
</Pagination>
</div>
</UiDialogContent>
</UiDialog>
</DialogContent>
</Dialog>
</template>

View File

@@ -37,13 +37,13 @@ const { getFormattedDateTitle } = useCalendar()
<menu class="flex gap-2 border-[1px] border-slate-700 rounded-sm w-fit">
<li v-if="character.birth">
<TooltipProvider :delay-duration="100">
<TooltipProvider :delayDuration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
@click="$emit('query:date-jump', character.birth!)"
variant="ghost"
size="xs"
@click="$emit('query:date-jump', character.birth!)"
>
<PhPlant size="16" weight="fill" />
{{ getFormattedDateTitle(character.birth, true) }}
@@ -57,13 +57,13 @@ const { getFormattedDateTitle } = useCalendar()
</li>
<span v-if="character.birth && character.death">-</span>
<li v-if="character.death">
<TooltipProvider :delay-duration="100">
<TooltipProvider :delayDuration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
@click="$emit('query:date-jump', character.death!)"
variant="ghost"
size="xs"
@click="$emit('query:date-jump', character.death!)"
>
<PhSkull size="16" weight="fill" />
{{ getFormattedDateTitle(character.death, true) }}
@@ -77,7 +77,7 @@ const { getFormattedDateTitle } = useCalendar()
</li>
</menu>
<hr v-if="character.description" class="border-white opacity-25" >
<hr v-if="character.description" class="border-white opacity-25" />
<div v-if="character.description" class="text-sm">
<span class="opacity-75">

View File

@@ -105,7 +105,7 @@ const dateDuration: string | null = props.event.endDate
</div>
<div v-if="event.description" class="text-sm">
<hr class="my-2 border-white opacity-50" >
<hr class="my-2 border-white opacity-50" />
<span class="opacity-75">
{{ event.description }}
</span>

View File

@@ -3,11 +3,12 @@ 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 { computed, ref } from 'vue'
import type { SearchMode } from '../../Search'
import CharacterCallout from './CharacterCallout.vue'
import EventCallout from './EventCallout.vue'
import { useScroll } from '@vueuse/core'
const props = defineProps<{
results: (Character | CalendarEvent)[]

View File

@@ -6,6 +6,7 @@ import { useElementBounding } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, ref, type ComputedRef } from 'vue'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import CalendarEventButton from '../../CalendarEvent.vue'
import type { CalendarEvent } from '@/models/Events'
@@ -73,17 +74,15 @@ const eventsNotDisplayed = computed(
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="{
'bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-blue-500': isSelectedDate
}"
>
{{ date.day }}
</span>
</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="{
'bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-blue-500': isSelectedDate
}"
>
{{ date.day }}
</span>
</button>
<ul
@@ -98,15 +97,15 @@ const eventsNotDisplayed = computed(
</li>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
<UiPopover>
<UiPopoverTrigger as-child>
<Popover>
<PopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
>
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
</button>
</UiPopoverTrigger>
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
</PopoverTrigger>
<PopoverContent class="w-80" :align="'center'" :side="'right'">
<div class="text-center mb-4">
<span
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
@@ -123,8 +122,8 @@ const eventsNotDisplayed = computed(
<CalendarEventButton :event :tile-date="date" />
</li>
</ul>
</UiPopoverContent>
</UiPopover>
</PopoverContent>
</Popover>
</li>
</ul>
</div>

View File

@@ -26,7 +26,7 @@ const moveCalendarRight = useThrottleFn(() => {
<template>
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
<div class="grid grid-cols-5 gap-x-8 gap-y-16">
<div ref="test" class="grid grid-cols-5 gap-x-8 gap-y-16">
<MonthTile
v-for="month in staticConfig.monthsPerYear"
:key="month"

View File

@@ -0,0 +1,54 @@
<script lang="ts" setup>
import { supabase } from '@/supabase/client'
import { type Session } from '@supabase/supabase-js'
import { computed, onMounted, ref, watch } from 'vue'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { PhUserCircle } from '@phosphor-icons/vue'
import Button from '@/components/ui/button/Button.vue'
import MenuLoggedIn from './MenuLoggedIn.vue'
import MenuLoggedOut from './MenuLoggedOut.vue'
const session = ref<Session | null>()
const hasSession = computed(() => Boolean(session.value))
const menuOpened = ref<boolean>(false)
onMounted(() => {
supabase.auth.getSession().then(({ data }) => {
session.value = data.session
})
supabase.auth.onAuthStateChange((_, _session) => {
session.value = _session
})
})
watch(session, closeMenu)
function closeMenu() {
menuOpened.value = false
}
</script>
<template>
<Popover v-model:open="menuOpened">
<PopoverTrigger :as-child="hasSession">
<Avatar v-if="session" class="cursor-pointer">
<AvatarImage
:src="session?.user.user_metadata.avatar_url"
:alt="session?.user.user_metadata.full_name"
referrerpolicy="no-referrer"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Button v-else variant="outline" size="icon">
<PhUserCircle size="18" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-fit p-0" :align="'start'" :side="'top'" :collision-padding="20">
<MenuLoggedIn v-if="session" :session />
<MenuLoggedOut v-else />
</PopoverContent>
</Popover>
</template>

View File

@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { supabase } from '@/supabase/client'
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import type { Session } from '@supabase/supabase-js'
import { useRouter } from 'vue-router'
defineProps<{
session: Session
}>()
const router = useRouter()
async function handleLogout() {
try {
const { error } = await supabase.auth.signOut()
if (error) throw error
} catch (err) {
console.log(err)
}
}
function gotoProfilePage() {
router.push({ path: '/profil' })
}
</script>
<template>
<Command>
<CommandList>
<CommandGroup :heading="`Connecté en tant que ${session.user.user_metadata.email}`">
<CommandItem value="profile" @select="gotoProfilePage"> Profil </CommandItem>
<CommandItem value="logout" @select="handleLogout"> Déconnexion </CommandItem>
</CommandGroup>
</CommandList>
</Command>
</template>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
import { supabase } from '@/supabase/client'
import { Command, CommandItem, CommandList } from '@/components/ui/command'
import type { Session } from '@supabase/supabase-js'
defineProps<{
session?: Session | null
}>()
async function handleGoogleLogin() {
try {
supabase.auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
} catch (err) {
console.log(err)
}
}
</script>
<template>
<Command>
<CommandList>
<CommandItem value="logout" @select="handleGoogleLogin"> Connexion </CommandItem>
</CommandList>
</Command>
</template>

View File

@@ -7,19 +7,18 @@ export const badgeVariants = cva(
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'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',
},
outline: 'text-foreground'
}
},
defaultVariants: {
variant: 'default',
},
},
variant: 'default'
}
}
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { RouterLink } from 'vue-router'
const props = defineProps<{
class?: HTMLAttributes['class']
@@ -19,7 +20,7 @@ const props = defineProps<{
>
<slot />
<NuxtLink
<RouterLink
v-if="props.link"
:to="props.link"
class="absolute inset-0 focus-visible:outline-none"

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<div :class="cn('p-6 pt-0 text-muted-foreground', props.class)">
<slot />
</div>
</template>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('text-sm text-muted-foreground', props.class)">
<p :class="cn('text-sm text-muted-foreground mt-1', props.class)">
<slot />
</div>
</p>
</template>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<div :class="cn('flex flex-col gap-y-1.5 px-6 pt-6 pb-3', props.class)">
<slot />
</div>
</template>

View File

@@ -8,11 +8,7 @@ const props = defineProps<{
</script>
<template>
<h3
:class="
cn('text-2xl font-semibold leading-none tracking-tight', props.class)
"
>
<h3 :class="cn('text-2xl font-semibold leading-none tracking-tight', props.class)">
<slot />
</h3>
</template>

View File

@@ -1,5 +1,3 @@
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'

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