Compare commits
2 Commits
0.1.0
...
features/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ee412ee0c | ||
|
|
521b3530a3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -28,3 +28,5 @@ coverage
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.env
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
|
<title>Outils — TTTools</title>
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon.png">
|
<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="32x32" href="/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
|||||||
141
package-lock.json
generated
141
package-lock.json
generated
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-project",
|
"name": "leim-tool",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vue-project",
|
"name": "leim-tool",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/vue": "^2.2.1",
|
"@phosphor-icons/vue": "^2.2.1",
|
||||||
|
"@supabase/supabase-js": "^2.43.1",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"lucide-vue-next": "^0.364.0",
|
"lucide-vue-next": "^0.364.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"radix-vue": "^1.6.1",
|
"radix-vue": "^1.6.1",
|
||||||
@@ -1143,6 +1145,73 @@
|
|||||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
|
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@supabase/auth-js": {
|
||||||
|
"version": "2.64.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz",
|
||||||
|
"integrity": "sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/functions-js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/node-fetch": {
|
||||||
|
"version": "2.6.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||||
|
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/postgrest-js": {
|
||||||
|
"version": "1.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz",
|
||||||
|
"integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/realtime-js": {
|
||||||
|
"version": "2.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.5.tgz",
|
||||||
|
"integrity": "sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14",
|
||||||
|
"@types/phoenix": "^1.5.4",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/storage-js": {
|
||||||
|
"version": "2.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/supabase-js": {
|
||||||
|
"version": "2.43.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.1.tgz",
|
||||||
|
"integrity": "sha512-A+RV50mWNtyKo6M0u4G6AOqEifQD+MoOjZcpRkPMPpEAFgMsc2dt3kBlBlR/MgZizWQgUKhsvrwKk0efc8g6Ug==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/auth-js": "2.64.2",
|
||||||
|
"@supabase/functions-js": "2.3.1",
|
||||||
|
"@supabase/node-fetch": "2.6.15",
|
||||||
|
"@supabase/postgrest-js": "1.15.2",
|
||||||
|
"@supabase/realtime-js": "2.9.5",
|
||||||
|
"@supabase/storage-js": "2.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.8",
|
"version": "0.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.8.tgz",
|
||||||
@@ -1173,11 +1242,15 @@
|
|||||||
"version": "20.12.2",
|
"version": "20.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz",
|
||||||
"integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==",
|
"integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/phoenix": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA=="
|
||||||
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.8",
|
"version": "7.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
||||||
@@ -1201,6 +1274,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
||||||
|
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
"version": "2.10.3",
|
"version": "2.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
@@ -4029,6 +4110,14 @@
|
|||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -5862,6 +5951,11 @@
|
|||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
||||||
@@ -5942,8 +6036,7 @@
|
|||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@@ -6199,6 +6292,20 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -6253,6 +6360,26 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||||
|
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||||
|
|||||||
@@ -18,10 +18,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/vue": "^2.2.1",
|
"@phosphor-icons/vue": "^2.2.1",
|
||||||
"lucide-vue-next": "^0.364.0",
|
"@supabase/supabase-js": "^2.43.1",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lucide-vue-next": "^0.364.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"radix-vue": "^1.6.1",
|
"radix-vue": "^1.6.1",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from '@vueuse/core'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
|
import { useAuth } from './stores/AuthStore'
|
||||||
|
|
||||||
useDark()
|
useDark()
|
||||||
|
useAuth()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
20
src/components/auth/Auth.vue
Normal file
20
src/components/auth/Auth.vue
Normal 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>
|
||||||
11
src/components/auth/Profile.vue
Normal file
11
src/components/auth/Profile.vue
Normal 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>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||||
|
|
||||||
import { Popover, PopoverTrigger } from '@/components/ui/popover'
|
|
||||||
import type { CalendarEvent } from '@/models/Events'
|
import type { CalendarEvent } from '@/models/Events'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import CalendarEventDetails from './CalendarEventDetails.vue'
|
import CalendarEventDetails from './CalendarEventDetails.vue'
|
||||||
|
import { Popover, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
event: CalendarEvent
|
event: CalendarEvent
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { useCalendar } from '@/stores/CalendarStore'
|
|||||||
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import Button from '../ui/button/Button.vue'
|
import Button from '../ui/button/Button.vue'
|
||||||
|
import UserCTA from '../global/user/CTA.vue'
|
||||||
|
|
||||||
const { revealAdvancedSearch } = useCalendar()
|
const { revealAdvancedSearch } = useCalendar()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid justify-center">
|
<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">
|
<menu class="flex flex-col gap-4">
|
||||||
<li class="mb-12">
|
<li class="mb-12">
|
||||||
<Button variant="ghost" size="icon" @click="console.log" class="rounded-full">
|
<Button variant="ghost" size="icon" @click="console.log" class="rounded-full">
|
||||||
@@ -52,5 +53,9 @@ const { revealAdvancedSearch } = useCalendar()
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UserCTA />
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
54
src/components/global/user/CTA.vue
Normal file
54
src/components/global/user/CTA.vue
Normal 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>
|
||||||
37
src/components/global/user/MenuLoggedIn.vue
Normal file
37
src/components/global/user/MenuLoggedIn.vue
Normal 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>
|
||||||
33
src/components/global/user/MenuLoggedOut.vue
Normal file
33
src/components/global/user/MenuLoggedOut.vue
Normal 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>
|
||||||
21
src/components/ui/avatar/Avatar.vue
Normal file
21
src/components/ui/avatar/Avatar.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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']
|
||||||
|
}>(), {
|
||||||
|
size: 'sm',
|
||||||
|
shape: 'circle',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
|
||||||
|
<slot />
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
11
src/components/ui/avatar/AvatarFallback.vue
Normal file
11
src/components/ui/avatar/AvatarFallback.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<AvatarFallbackProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarFallback v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</AvatarFallback>
|
||||||
|
</template>
|
||||||
9
src/components/ui/avatar/AvatarImage.vue
Normal file
9
src/components/ui/avatar/AvatarImage.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<AvatarImageProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
||||||
|
</template>
|
||||||
24
src/components/ui/avatar/index.ts
Normal file
24
src/components/ui/avatar/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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 const avatarVariant = cva(
|
||||||
|
'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',
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
circle: 'rounded-full',
|
||||||
|
square: 'rounded-md',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AvatarVariants = VariantProps<typeof avatarVariant>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomePage from '@/views/HomePage.vue'
|
import HomePage from '@/views/HomePage.vue'
|
||||||
import CalendarPage from '@/views/CalendarPage.vue'
|
import CalendarPage from '@/views/CalendarPage.vue'
|
||||||
|
import ProfilePage from '@/views/ProfilePage.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -14,6 +15,11 @@ const router = createRouter({
|
|||||||
path: '/calendrier',
|
path: '/calendrier',
|
||||||
name: 'calendrier',
|
name: 'calendrier',
|
||||||
component: CalendarPage
|
component: CalendarPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profil',
|
||||||
|
name: 'profil',
|
||||||
|
component: ProfilePage
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
32
src/stores/AuthStore.ts
Normal file
32
src/stores/AuthStore.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { supabase } from '@/supabase/client'
|
||||||
|
import type { Session } from '@supabase/supabase-js'
|
||||||
|
import { jwtDecode, type JwtPayload } from 'jwt-decode'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
interface JwtPayloadExt extends JwtPayload {
|
||||||
|
user_role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = defineStore('auth', () => {
|
||||||
|
const session = ref<Session | null>()
|
||||||
|
const userRoles = computed(() => {
|
||||||
|
if (session.value) {
|
||||||
|
return (jwtDecode(session.value.access_token) as JwtPayloadExt).user_role
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadSession() {
|
||||||
|
return supabase.auth.getSession().then(({ data }) => {
|
||||||
|
session.value = data.session
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
supabase.auth.onAuthStateChange((_, _session) => {
|
||||||
|
session.value = _session
|
||||||
|
})
|
||||||
|
|
||||||
|
return { session, userRoles, loadSession }
|
||||||
|
})
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
type LeimPeriod,
|
type LeimPeriod,
|
||||||
type LeimPeriodShort
|
type LeimPeriodShort
|
||||||
} from '@/models/Date'
|
} from '@/models/Date'
|
||||||
import { isDigit, isInt, isSignedInt } from '@/utils/Regex'
|
|
||||||
import { useStorage, useUrlSearchParams } from '@vueuse/core'
|
import { useStorage, useUrlSearchParams } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
||||||
@@ -76,11 +75,6 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
'century'
|
'century'
|
||||||
])
|
])
|
||||||
|
|
||||||
// Get date from URL params
|
|
||||||
const params = useUrlSearchParams('history', {
|
|
||||||
write: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Default date settings (current day in the world)
|
// Default date settings (current day in the world)
|
||||||
const defaultDay: number = 23
|
const defaultDay: number = 23
|
||||||
const defaultMonth: number = 8
|
const defaultMonth: number = 8
|
||||||
@@ -93,6 +87,16 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get date from URL params
|
||||||
|
const params = useUrlSearchParams('history', {
|
||||||
|
write: false,
|
||||||
|
initialValue: {
|
||||||
|
day: defaultDate.value.day.toString(),
|
||||||
|
month: defaultDate.value.month.toString(),
|
||||||
|
year: defaultDate.value.year.toString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const currentDay = computed<number>(() => {
|
const currentDay = computed<number>(() => {
|
||||||
return Number(params.day)
|
return Number(params.day)
|
||||||
})
|
})
|
||||||
@@ -180,19 +184,6 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function validateUrlParams(): void {
|
|
||||||
// Assign default values if no params exist in URL
|
|
||||||
if (!params.day || typeof params.day === 'object' || !isInt(params.day)) {
|
|
||||||
params.day = defaultDate.value.day.toString()
|
|
||||||
}
|
|
||||||
if (!params.month || typeof params.month === 'object' || !isDigit(params.month)) {
|
|
||||||
params.month = defaultDate.value.month.toString()
|
|
||||||
}
|
|
||||||
if (!params.year || typeof params.year === 'object' || !isSignedInt(params.year)) {
|
|
||||||
params.year = defaultDate.value.year.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves the current date forward one month
|
* Moves the current date forward one month
|
||||||
*/
|
*/
|
||||||
@@ -404,7 +395,6 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
selectedDate,
|
selectedDate,
|
||||||
selectDate,
|
selectDate,
|
||||||
params,
|
params,
|
||||||
validateUrlParams,
|
|
||||||
getPeriodOfYear,
|
getPeriodOfYear,
|
||||||
incrementMonth,
|
incrementMonth,
|
||||||
decrementMonth,
|
decrementMonth,
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import type { CalendarEvent } from '@/models/Events'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, watch, type Ref } from 'vue'
|
import { ref, watch, type Ref } from 'vue'
|
||||||
import { useCalendar } from './CalendarStore'
|
import { useCalendar } from './CalendarStore'
|
||||||
|
import { useAuth } from './AuthStore'
|
||||||
// import { useCharacters } from './CharacterStore'
|
// import { useCharacters } from './CharacterStore'
|
||||||
|
|
||||||
export const useCalendarEvents = defineStore('calendar-events', () => {
|
export const useCalendarEvents = defineStore('calendar-events', () => {
|
||||||
|
const authStore = useAuth()
|
||||||
const { currentDate, currentConfig } = useCalendar()
|
const { currentDate, currentConfig } = useCalendar()
|
||||||
// const { charactersWithBirthData, charactersWithDeathData } = useCharacters()
|
// const { charactersWithBirthData, charactersWithDeathData } = useCharacters()
|
||||||
|
|
||||||
const baseEvents: CalendarEvent[] = initialEvents
|
|
||||||
|
|
||||||
// const characterBirthEvents = charactersWithBirthData.map((character) => {
|
// const characterBirthEvents = charactersWithBirthData.map((character) => {
|
||||||
// return {
|
// return {
|
||||||
// title: `Naissance de ${character.name}`,
|
// title: `Naissance de ${character.name}`,
|
||||||
@@ -28,9 +28,13 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
// } as CalendarEvent
|
// } as CalendarEvent
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const allEvents = [...baseEvents].sort((a, b) => {
|
const allEvents: CalendarEvent[] = [...initialEvents]
|
||||||
return compareDates(a.startDate, b.startDate, 'desc')
|
.filter((event) => {
|
||||||
})
|
return !event.hidden || (event.hidden && ['sa', 'gm'].includes(authStore.userRoles))
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return compareDates(a.startDate, b.startDate, 'desc')
|
||||||
|
})
|
||||||
|
|
||||||
// Gets all current event in its default state
|
// Gets all current event in its default state
|
||||||
const currentEvents: Ref<CalendarEvent[]> = ref(computeCurrentEvents())
|
const currentEvents: Ref<CalendarEvent[]> = ref(computeCurrentEvents())
|
||||||
@@ -48,7 +52,7 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
* @param event The event to analyze
|
* @param event The event to analyze
|
||||||
* @returns Whether the event should appear in the current view
|
* @returns Whether the event should appear in the current view
|
||||||
*/
|
*/
|
||||||
function shouldEventBeDisplayed(event: CalendarEvent): boolean {
|
function shouldEventBeDrawn(event: CalendarEvent): boolean {
|
||||||
const eventStartDateToDays = convertDateToDays(event.startDate)
|
const eventStartDateToDays = convertDateToDays(event.startDate)
|
||||||
const eventEndDateToDays: number = event.endDate ? convertDateToDays(event.endDate) : 0
|
const eventEndDateToDays: number = event.endDate ? convertDateToDays(event.endDate) : 0
|
||||||
|
|
||||||
@@ -105,7 +109,7 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
* @returns A list of events that can appear in the current view
|
* @returns A list of events that can appear in the current view
|
||||||
*/
|
*/
|
||||||
function computeCurrentEvents(): CalendarEvent[] {
|
function computeCurrentEvents(): CalendarEvent[] {
|
||||||
return allEvents.filter((event) => shouldEventBeDisplayed(event))
|
return allEvents.filter((event) => shouldEventBeDrawn(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,27 +172,6 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Special case : If we query the first one but it already is
|
|
||||||
// if (position === 'prev' && t[0].distance === 0) {
|
|
||||||
// const targetDate =
|
|
||||||
// t[0].targetKey === 'startDate' ? t[0].eventData.startDate : t[0].eventData.endDate!
|
|
||||||
// return {
|
|
||||||
// event: t[0].eventData,
|
|
||||||
// targetDate: targetDate
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // Special case : If we query the last one but it already is
|
|
||||||
// if (position === 'next' && t[t.length - 1].distance === 0) {
|
|
||||||
// const targetDate =
|
|
||||||
// t[t.length - 1].targetKey === 'startDate'
|
|
||||||
// ? t[t.length - 1].eventData.startDate
|
|
||||||
// : t[t.length - 1].eventData.endDate!
|
|
||||||
// return {
|
|
||||||
// event: t[t.length - 1].eventData,
|
|
||||||
// targetDate: targetDate
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Based on the direction, either ignore negative distance (past) or positive distance (future)
|
// Based on the direction, either ignore negative distance (past) or positive distance (future)
|
||||||
t = t.filter((i) => {
|
t = t.filter((i) => {
|
||||||
return position === 'next' ? i.distance > 0 : i.distance < 0
|
return position === 'next' ? i.distance > 0 : i.distance < 0
|
||||||
|
|||||||
6
src/supabase/client.ts
Normal file
6
src/supabase/client.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||||
|
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||||
@@ -1,20 +1,60 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { useCalendar } from '@/stores/CalendarStore'
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
import { isDigit, isInt, isSignedInt } from '@/utils/Regex'
|
||||||
|
import { titleTemplate } from '@/utils/meta'
|
||||||
import { useTitle } from '@vueuse/core'
|
import { useTitle } from '@vueuse/core'
|
||||||
import { titleTemplate } from './meta'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { onBeforeMount } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import Calendar from '@/components/calendar/Calendar.vue'
|
import Calendar from '@/components/calendar/Calendar.vue'
|
||||||
import Sidebar from '@/components/calendar/Sidebar.vue'
|
import Sidebar from '@/components/calendar/Sidebar.vue'
|
||||||
import CalendarSearch from '@/components/calendar/search/CalendarSearch.vue'
|
import CalendarSearch from '@/components/calendar/search/CalendarSearch.vue'
|
||||||
|
|
||||||
const { validateUrlParams } = useCalendar()
|
useTitle('Calendrier', titleTemplate)
|
||||||
|
|
||||||
|
const { defaultDate, selectedDate, jumpToDate } = useCalendar()
|
||||||
const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
|
const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
onMounted(() => validateUrlParams())
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
useTitle('Calendrier', titleTemplate)
|
// Check whether all the params are good to go before rendering anything
|
||||||
|
onBeforeMount(() => {
|
||||||
|
let isInvalid: boolean = false
|
||||||
|
|
||||||
|
// If the user already has a selected date from session, jump to it
|
||||||
|
if (selectedDate) {
|
||||||
|
jumpToDate(selectedDate)
|
||||||
|
} else {
|
||||||
|
// Assign default values if no params exist in URL
|
||||||
|
if (!route.query.day || typeof route.query.day === 'object' || !isInt(route.query.day)) {
|
||||||
|
route.query.day = defaultDate.day.toString()
|
||||||
|
isInvalid = true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!route.query.month ||
|
||||||
|
typeof route.query.month === 'object' ||
|
||||||
|
!isDigit(route.query.month)
|
||||||
|
) {
|
||||||
|
route.query.month = defaultDate.month.toString()
|
||||||
|
isInvalid = true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!route.query.year ||
|
||||||
|
typeof route.query.year === 'object' ||
|
||||||
|
!isSignedInt(route.query.year)
|
||||||
|
) {
|
||||||
|
route.query.year = defaultDate.year.toString()
|
||||||
|
isInvalid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInvalid) {
|
||||||
|
console.log('replaced')
|
||||||
|
router.replace({ query: route.query, replace: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTitle } from '@vueuse/core'
|
import { useTitle } from '@vueuse/core'
|
||||||
import { titleTemplate } from './meta'
|
import { titleTemplate } from '@/utils/meta'
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|||||||
25
src/views/ProfilePage.vue
Normal file
25
src/views/ProfilePage.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import Profile from '@/components/auth/Profile.vue'
|
||||||
|
import Auth from '@/components/auth/Auth.vue'
|
||||||
|
import { supabase } from '@/supabase/client'
|
||||||
|
|
||||||
|
const session = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
supabase.auth.getSession().then(({ data }) => {
|
||||||
|
session.value = data.session
|
||||||
|
})
|
||||||
|
|
||||||
|
supabase.auth.onAuthStateChange((_, _session) => {
|
||||||
|
session.value = _session
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container" style="padding: 50px 0 100px 0">
|
||||||
|
<Profile v-if="session" :session="session" />
|
||||||
|
<Auth v-else />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
4
supabase/.gitignore
vendored
Normal file
4
supabase/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Supabase
|
||||||
|
.branches
|
||||||
|
.temp
|
||||||
|
.env
|
||||||
128
supabase/config.toml
Normal file
128
supabase/config.toml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||||
|
# working directory name when running `supabase init`.
|
||||||
|
project_id = "leim-tools"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the API URL.
|
||||||
|
port = 54321
|
||||||
|
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||||
|
# endpoints. `public` is always included.
|
||||||
|
schemas = ["public", "graphql_public"]
|
||||||
|
# Extra schemas to add to the search_path of every request. `public` is always included.
|
||||||
|
extra_search_path = ["public", "extensions"]
|
||||||
|
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||||
|
# for accidental or malicious requests.
|
||||||
|
max_rows = 1000
|
||||||
|
|
||||||
|
[db]
|
||||||
|
# Port to use for the local database URL.
|
||||||
|
port = 54322
|
||||||
|
# Port used by db diff command to initialize the shadow database.
|
||||||
|
shadow_port = 54320
|
||||||
|
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||||
|
# server_version;` on the remote database to check.
|
||||||
|
major_version = 15
|
||||||
|
|
||||||
|
[db.pooler]
|
||||||
|
enabled = false
|
||||||
|
# Port to use for the local connection pooler.
|
||||||
|
port = 54329
|
||||||
|
# Specifies when a server connection can be reused by other clients.
|
||||||
|
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||||
|
pool_mode = "transaction"
|
||||||
|
# How many server connections to allow per user/database pair.
|
||||||
|
default_pool_size = 20
|
||||||
|
# Maximum number of client connections allowed.
|
||||||
|
max_client_conn = 100
|
||||||
|
|
||||||
|
[realtime]
|
||||||
|
enabled = true
|
||||||
|
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||||
|
# ip_version = "IPv6"
|
||||||
|
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||||
|
# max_header_length = 4096
|
||||||
|
|
||||||
|
[studio]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for Supabase Studio.
|
||||||
|
port = 54323
|
||||||
|
# External URL of the API server that frontend connects to.
|
||||||
|
api_url = "http://127.0.0.1"
|
||||||
|
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||||
|
openai_api_key = "env(OPENAI_API_KEY)"
|
||||||
|
|
||||||
|
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||||
|
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||||
|
[inbucket]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the email testing server web interface.
|
||||||
|
port = 54324
|
||||||
|
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||||
|
# smtp_port = 54325
|
||||||
|
# pop3_port = 54326
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
enabled = true
|
||||||
|
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||||
|
file_size_limit = "50MiB"
|
||||||
|
|
||||||
|
[storage.image_transformation]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
enabled = true
|
||||||
|
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||||
|
# in emails.
|
||||||
|
site_url = "http://localhost:5173/"
|
||||||
|
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||||
|
additional_redirect_urls = ["http://localhost:5173/"]
|
||||||
|
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||||
|
jwt_expiry = 3600
|
||||||
|
# If disabled, the refresh token will never expire.
|
||||||
|
enable_refresh_token_rotation = true
|
||||||
|
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||||
|
# Requires enable_refresh_token_rotation = true.
|
||||||
|
refresh_token_reuse_interval = 10
|
||||||
|
# Allow/disallow new user signups to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# Allow/disallow anonymous sign-ins to your project.
|
||||||
|
enable_anonymous_sign_ins = false
|
||||||
|
# Allow/disallow testing manual linking of accounts
|
||||||
|
enable_manual_linking = false
|
||||||
|
|
||||||
|
[auth.email]
|
||||||
|
# Allow/disallow new user signups via email to your project.
|
||||||
|
enable_signup = false
|
||||||
|
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||||
|
# addresses. If disabled, only the new email is required to confirm.
|
||||||
|
double_confirm_changes = false
|
||||||
|
# If enabled, users need to confirm their email address before signing in.
|
||||||
|
enable_confirmations = false
|
||||||
|
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||||
|
max_frequency = "1s"
|
||||||
|
|
||||||
|
# Uncomment to customize email template
|
||||||
|
# [auth.email.template.invite]
|
||||||
|
# subject = "You have been invited"
|
||||||
|
# content_path = "./supabase/templates/invite.html"
|
||||||
|
|
||||||
|
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||||
|
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||||
|
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||||
|
[auth.external.google]
|
||||||
|
enabled = true
|
||||||
|
client_id = "env(GOOGLE_AUTH_ID)"
|
||||||
|
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||||
|
secret = "env(GOOGLE_AUTH_SECRET)"
|
||||||
|
# Overrides the default auth redirectUrl.
|
||||||
|
redirect_uri = ""
|
||||||
|
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||||
|
# or any other third-party OIDC providers.
|
||||||
|
url = ""
|
||||||
|
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||||
|
skip_nonce_check = false
|
||||||
|
|
||||||
|
[auth.hook.custom_access_token]
|
||||||
|
enabled = true
|
||||||
|
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
||||||
103
supabase/migrations/202401_init.sql
Normal file
103
supabase/migrations/202401_init.sql
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
--
|
||||||
|
-- For use with https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Custom types
|
||||||
|
create type public.app_permission as enum ('events.see.hidden');
|
||||||
|
create type public.app_role as enum ('sa', 'gm');
|
||||||
|
|
||||||
|
-- USERS
|
||||||
|
create table public.users (
|
||||||
|
id uuid references auth.users not null primary key, -- UUID from auth.users
|
||||||
|
username text
|
||||||
|
);
|
||||||
|
comment on table public.users is 'Profile data for each user.';
|
||||||
|
comment on column public.users.id is 'References the internal Supabase Auth user.';
|
||||||
|
|
||||||
|
-- USER ROLES
|
||||||
|
create table public.user_roles (
|
||||||
|
id bigint generated by default as identity primary key,
|
||||||
|
user_id uuid references public.users on delete cascade not null,
|
||||||
|
role app_role not null,
|
||||||
|
unique (user_id, role)
|
||||||
|
);
|
||||||
|
comment on table public.user_roles is 'Application roles for each user.';
|
||||||
|
|
||||||
|
-- ROLE PERMISSIONS
|
||||||
|
create table public.role_permissions (
|
||||||
|
id bigint generated by default as identity primary key,
|
||||||
|
role app_role not null,
|
||||||
|
permission app_permission not null,
|
||||||
|
unique (role, permission)
|
||||||
|
);
|
||||||
|
comment on table public.role_permissions is 'Application permissions for each role.';
|
||||||
|
|
||||||
|
-- authorize with role-based access control (RBAC)
|
||||||
|
create function public.authorize(
|
||||||
|
requested_permission app_permission
|
||||||
|
)
|
||||||
|
returns boolean as $$
|
||||||
|
declare
|
||||||
|
bind_permissions int;
|
||||||
|
begin
|
||||||
|
select count(*)
|
||||||
|
from public.role_permissions
|
||||||
|
where role_permissions.permission = authorize.requested_permission
|
||||||
|
and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
|
||||||
|
into bind_permissions;
|
||||||
|
|
||||||
|
return bind_permissions > 0;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql security definer set search_path = public;
|
||||||
|
|
||||||
|
-- Secure the tables
|
||||||
|
alter table public.users enable row level security;
|
||||||
|
alter table public.user_roles enable row level security;
|
||||||
|
alter table public.role_permissions enable row level security;
|
||||||
|
create policy "Allow logged-in read access" on public.users for select using ( auth.role() = 'authenticated' );
|
||||||
|
create policy "Allow individual insert access" on public.users for insert with check ( auth.uid() = id );
|
||||||
|
create policy "Allow individual update access" on public.users for update using ( auth.uid() = id );
|
||||||
|
create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id );
|
||||||
|
|
||||||
|
-- Send "previous data" on change
|
||||||
|
alter table public.users replica identity full;
|
||||||
|
|
||||||
|
-- inserts a row into public.users and assigns roles
|
||||||
|
create function public.handle_new_user()
|
||||||
|
returns trigger as $$
|
||||||
|
declare is_admin boolean;
|
||||||
|
begin
|
||||||
|
insert into public.users (id, username)
|
||||||
|
values (new.id, new.email);
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql security definer set search_path = auth, public;
|
||||||
|
|
||||||
|
-- trigger the function every time a user is created
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row execute procedure public.handle_new_user();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HELPER FUNCTIONS
|
||||||
|
* Create test user helper method.
|
||||||
|
*/
|
||||||
|
create or replace function public.create_user(
|
||||||
|
email text
|
||||||
|
) returns uuid
|
||||||
|
security definer
|
||||||
|
set search_path = auth
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
user_id uuid;
|
||||||
|
begin
|
||||||
|
user_id := extensions.uuid_generate_v4();
|
||||||
|
|
||||||
|
insert into auth.users (id, email)
|
||||||
|
values (user_id, email)
|
||||||
|
returning id into user_id;
|
||||||
|
|
||||||
|
return user_id;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
58
supabase/migrations/202402_auth-hook.sql
Normal file
58
supabase/migrations/202402_auth-hook.sql
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* AUTH HOOKS
|
||||||
|
* Create an auth hook to add a custom claim to the access token jwt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Create the auth hook function
|
||||||
|
-- https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token
|
||||||
|
create or replace function public.custom_access_token_hook(event jsonb)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
claims jsonb;
|
||||||
|
user_role public.app_role;
|
||||||
|
begin
|
||||||
|
-- Check if the user is marked as admin in the profiles table
|
||||||
|
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
|
||||||
|
|
||||||
|
claims := event->'claims';
|
||||||
|
|
||||||
|
if user_role is not null then
|
||||||
|
-- Set the claim
|
||||||
|
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
|
||||||
|
else
|
||||||
|
claims := jsonb_set(claims, '{user_role}', 'null');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Update the 'claims' object in the original event
|
||||||
|
event := jsonb_set(event, '{claims}', claims);
|
||||||
|
|
||||||
|
-- Return the modified or original event
|
||||||
|
return event;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant usage on schema public to supabase_auth_admin;
|
||||||
|
|
||||||
|
grant execute
|
||||||
|
on function public.custom_access_token_hook
|
||||||
|
to supabase_auth_admin;
|
||||||
|
|
||||||
|
revoke execute
|
||||||
|
on function public.custom_access_token_hook
|
||||||
|
from authenticated, anon;
|
||||||
|
|
||||||
|
grant all
|
||||||
|
on table public.user_roles
|
||||||
|
to supabase_auth_admin;
|
||||||
|
|
||||||
|
revoke all
|
||||||
|
on table public.user_roles
|
||||||
|
from authenticated, anon;
|
||||||
|
|
||||||
|
create policy "Allow auth admin to read user roles" ON public.user_roles
|
||||||
|
as permissive for select
|
||||||
|
to supabase_auth_admin
|
||||||
|
using (true)
|
||||||
1
supabase/seed.sql
Normal file
1
supabase/seed.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
insert into public.role_permissions (role, permission) values ('gm', 'events.see.hidden');
|
||||||
Reference in New Issue
Block a user