Version 1.0.0 - Initialisation du depot
4
.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# **Subsonics - Chopin Frontend**
|
||||
|
||||
> Cette version est une refonte complète et intégrale de [Subsonics - Web](https://git.raphix.fr/subsonics/web)
|
||||
|
||||
Ce dépot est le frontend de [Subsonics Chopin](https://git.raphix.fr/subsonics/chopin)
|
||||
|
||||
## Bienvenue sur Chopin, la nouvelle version de Subsonics
|
||||
|
||||
## **Fonctionnalités**
|
||||
|
||||
> - Lecture de vidéos depuis Youtube, Spotify et SoundClound
|
||||
> - Lecture de fichiers locaux *(Uniquement sur le site)*
|
||||
> - Gestion et lecture de playlist
|
||||
> - Accéder à votre propre historique et à l'historique du Bot
|
||||
> - Affichage des paroles de la musique en cours
|
||||
> - Une interface refaite pour toutes les platformes.
|
||||
> - Récupération de vos recommendations & Playlists Spotify / Youtube
|
||||
|
||||
Le FrontEnd est gérée par VueJS ✌️et le BackEnd a été entièrement refait localement pour des réponses plus rapide et plus stable
|
||||
|
||||
|
||||
[CHANGELOG](https://git.raphix.fr/subsonics/chopin/src/branch/main/changelog.md)
|
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Subsonics doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
19
jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
2183
package-lock.json
generated
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "chopin-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --host --port 8080",
|
||||
"build": "vue-tsc -b && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@unhead/vue": "^2.0.12",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"core-js": "^3.8.3",
|
||||
"pinia": "^3.0.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vite": "^7.0.5",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3"
|
||||
}
|
||||
}
|
BIN
public/discord-logo-white.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
public/gunship.ttf
Normal file
14
public/information.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"discord": {
|
||||
"development": "https://discord.com/oauth2/authorize?client_id=1342913183744004158&response_type=code&redirect_uri=http%3A%2F%2F192.168.1.77%3A8080%2Fredirect&scope=guilds+identify+guilds.members.read",
|
||||
"production": "https://discord.com/oauth2/authorize?client_id=1094727789682380922&response_type=code&redirect_uri=https%3A%2F%2Fsubsonics.raphix.fr%2Fredirect&scope=guilds+identify+guilds.members.read"
|
||||
},
|
||||
"backend": {
|
||||
"development": "http://192.168.1.77:3000",
|
||||
"production": "http://backend.subsonics.raphix.fr"
|
||||
},
|
||||
"bot_invite": {
|
||||
"development": "https://discord.com/api/oauth2/authorize?client_id=1342913183744004158&permissions=40546675842624&scope=bot%20applications.commands",
|
||||
"production": "https://discord.com/api/oauth2/authorize?client_id=1094727789682380922&permissions=40546675842624&scope=bot%20applications.commands"
|
||||
}
|
||||
}
|
52
public/logo-white.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
inkscape:export-filename="logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.0595067"
|
||||
inkscape:cx="98.158891"
|
||||
inkscape:cy="738.55127"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2054"
|
||||
inkscape:window-x="2869"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
id="g1"
|
||||
transform="translate(-7.748334,-6.62121)">
|
||||
<path
|
||||
d="m 548.34145,130.19117 c 0,115.54868 -0.16071,117.9593 -10.60669,152.67212 -10.12458,33.10576 -27.15957,66.69362 -50.30145,98.03158 -6.1069,8.19609 -26.99887,32.78433 -46.44447,54.64055 -41.46255,46.44447 -55.4441,63.47948 -72.4791,88.71055 -35.67706,52.71207 -54.15838,100.92431 -59.78313,157.01124 -8.99964,88.38913 37.28408,178.86745 114.7451,224.18697 10.28528,5.94617 31.01656,15.26722 40.98041,18.15995 l 3.53557,0.96423 V 777.84243 c 0,-155.24346 0.32142,-163.11813 7.87467,-194.29539 9.80316,-41.30183 29.8916,-78.74668 65.56865,-122.45912 24.26685,-29.8916 52.22995,-67.49715 62.99735,-84.85357 35.51637,-57.21187 53.3549,-113.94161 51.105,-162.79669 C 654.08699,180.1712 646.37303,151.08314 631.10583,120.0666 618.40993,94.6748 604.58907,75.068476 585.7863,56.105001 573.2511,43.569813 552.03772,26.05272 549.30569,26.05272 c -0.48212,0 -0.96424,46.926551 -0.96424,104.13845 z"
|
||||
fill="#ffffff"
|
||||
id="path1"
|
||||
style="stroke-width:1.60707" />
|
||||
<path
|
||||
d="m 522.62825,760.3253 v 250.8644 h 6.10687 c 3.21417,0 6.91044,-0.6429 8.03538,-1.2856 1.60709,-1.125 1.9285,-36.48067 1.60709,-173.56414 -0.48213,-139.81555 -0.16071,-172.27846 1.60707,-172.27846 8.35679,0 25.23107,14.46368 31.65938,27.48099 8.5175,16.55287 8.83891,21.21339 8.83891,130.33379 0,83.72861 0.32142,100.9243 2.2499,100.9243 1.12496,0 11.41024,-4.66051 22.82047,-10.44597 66.3722,-33.26645 111.3703,-92.0854 123.58408,-161.51106 3.21415,-17.83853 3.69627,-53.67631 0.96424,-69.26493 -5.14264,-30.37371 -19.4456,-64.283 -37.28415,-88.38913 -36.6413,-49.3372 -90.63903,-78.26456 -154.4399,-82.76435 l -15.74934,-1.12496 z"
|
||||
fill="#ffffff"
|
||||
id="path2"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:1.60707" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/logo.png
Normal file
After Width: | Height: | Size: 24 KiB |
52
public/logo.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
inkscape:export-filename="logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.0595067"
|
||||
inkscape:cx="98.158891"
|
||||
inkscape:cy="738.55127"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2054"
|
||||
inkscape:window-x="2869"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
id="g1"
|
||||
transform="translate(-7.748334,-6.62121)">
|
||||
<path
|
||||
d="m 548.34145,130.19117 c 0,115.54868 -0.16071,117.9593 -10.60669,152.67212 -10.12458,33.10576 -27.15957,66.69362 -50.30145,98.03158 -6.1069,8.19609 -26.99887,32.78433 -46.44447,54.64055 -41.46255,46.44447 -55.4441,63.47948 -72.4791,88.71055 -35.67706,52.71207 -54.15838,100.92431 -59.78313,157.01124 -8.99964,88.38913 37.28408,178.86745 114.7451,224.18697 10.28528,5.94617 31.01656,15.26722 40.98041,18.15995 l 3.53557,0.96423 V 777.84243 c 0,-155.24346 0.32142,-163.11813 7.87467,-194.29539 9.80316,-41.30183 29.8916,-78.74668 65.56865,-122.45912 24.26685,-29.8916 52.22995,-67.49715 62.99735,-84.85357 35.51637,-57.21187 53.3549,-113.94161 51.105,-162.79669 C 654.08699,180.1712 646.37303,151.08314 631.10583,120.0666 618.40993,94.6748 604.58907,75.068476 585.7863,56.105001 573.2511,43.569813 552.03772,26.05272 549.30569,26.05272 c -0.48212,0 -0.96424,46.926551 -0.96424,104.13845 z"
|
||||
fill="#ffffff"
|
||||
id="path1"
|
||||
style="stroke-width:1.60707" />
|
||||
<path
|
||||
d="m 522.62825,760.3253 v 250.8644 h 6.10687 c 3.21417,0 6.91044,-0.6429 8.03538,-1.2856 1.60709,-1.125 1.9285,-36.48067 1.60709,-173.56414 -0.48213,-139.81555 -0.16071,-172.27846 1.60707,-172.27846 8.35679,0 25.23107,14.46368 31.65938,27.48099 8.5175,16.55287 8.83891,21.21339 8.83891,130.33379 0,83.72861 0.32142,100.9243 2.2499,100.9243 1.12496,0 11.41024,-4.66051 22.82047,-10.44597 66.3722,-33.26645 111.3703,-92.0854 123.58408,-161.51106 3.21415,-17.83853 3.69627,-53.67631 0.96424,-69.26493 -5.14264,-30.37371 -19.4456,-64.283 -37.28415,-88.38913 -36.6413,-49.3372 -90.63903,-78.26456 -154.4399,-82.76435 l -15.74934,-1.12496 z"
|
||||
fill="#7A258D"
|
||||
id="path2"
|
||||
style="fill:#cd034f;fill-opacity:1;stroke-width:1.60707" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/logoblack.png
Normal file
After Width: | Height: | Size: 27 KiB |
54
public/logoblack.svg
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="logoblack.svg"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
inkscape:export-filename="logoblack.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.37459218"
|
||||
inkscape:cx="-413.78333"
|
||||
inkscape:cy="385.75285"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2054"
|
||||
inkscape:window-x="2869"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
id="g1"
|
||||
transform="matrix(1.6150014,0,0,1.6150014,-132.31608,-233.05676)">
|
||||
<path
|
||||
d="m 416.749,219.63505 c 0,71.9 -0.1,73.4 -6.6,95 -6.3,20.6 -16.9,41.5 -31.3,61 -3.8,5.1 -16.8,20.4 -28.9,34 -25.8,28.9 -34.5,39.5 -45.1,55.2 -22.2,32.8 -33.69997,62.8 -37.19997,97.7 -5.6,55 23.19997,111.3 71.39997,139.5 6.4,3.7 19.3,9.5 25.5,11.3 l 2.2,0.6 v -91.3 c 0,-96.6 0.2,-101.5 4.9,-120.9 6.1,-25.7 18.6,-49 40.8,-76.2 15.1,-18.6 32.5,-42 39.2,-52.8 22.1,-35.6 33.2,-70.9 31.8,-101.3 -0.9,-20.7 -5.7,-38.8 -15.2,-58.1 -7.9,-15.8 -16.5,-28 -28.2,-39.8 -7.8,-7.8 -21,-18.69999 -22.7,-18.69999 -0.3,0 -0.6,29.19999 -0.6,64.79999 z"
|
||||
fill="white"
|
||||
id="path1"
|
||||
style="fill:#111210;fill-opacity:1"
|
||||
sodipodi:nodetypes="sccccccccsccccccss" />
|
||||
<path
|
||||
d="m 400.749,611.73505 v 156.1 h 3.8 c 2,0 4.3,-0.4 5,-0.8 1,-0.7 1.2,-22.7 1,-108 -0.3,-87 -0.1,-107.2 1,-107.2 5.2,0 15.7,9 19.7,17.1 5.3,10.3 5.5,13.2 5.5,81.1 0,52.1 0.2,62.8 1.4,62.8 0.7,0 7.1,-2.9 14.2,-6.5 41.3,-20.7 69.3,-57.3 76.9,-100.5 2,-11.1 2.3,-33.4 0.6,-43.1 -3.2,-18.9 -12.1,-40 -23.2,-55 -22.8,-30.7 -56.4,-48.7 -96.1,-51.5 l -9.8,-0.7 z"
|
||||
fill="#7A258D"
|
||||
id="path2"
|
||||
style="fill:#cd034f;fill-opacity:1"
|
||||
sodipodi:nodetypes="ccsccscssccccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
14
src/App.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
});
|
||||
console.log("Subsonics Chopin - App Vue Loaded");
|
||||
|
||||
</script>
|
84
src/assets/Global.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
@font-face {
|
||||
font-family: 'Gunship';
|
||||
src: url("/gunship.ttf");
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--main: #CD034F;
|
||||
--main-hover: #A0023F;
|
||||
--main-active: #7A002F;
|
||||
|
||||
--primary: #111210;
|
||||
--secondary: #2A2B28;
|
||||
--tertiary: #404040;
|
||||
--text: #FFFFFF;
|
||||
--text-secondary: #C5c3c3;
|
||||
--text-error: #ff2b2b;
|
||||
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
--main: #CD034F;
|
||||
--main-hover: #A0023F;
|
||||
--main-active: #7A002F;
|
||||
|
||||
--primary: #FFFFFF;
|
||||
--secondary: #EAEAEA;
|
||||
--tertiary: #C5c3c3;
|
||||
--text: #111210;
|
||||
--text-secondary: #404040;
|
||||
--text-error: #CD034F;
|
||||
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0 !important;
|
||||
font-family: 'Inter', sans-serif;
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--primary);
|
||||
|
||||
}
|
||||
|
||||
.no-decoration {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Gunship';
|
||||
font-size: 30px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-secondary)
|
||||
}
|
||||
|
||||
/*Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ffffff56;
|
||||
border-radius: 5px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #ffffffa8;
|
||||
}
|
||||
|
30
src/components/Actions/ReturnHomeButton.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<IconAction color="white" class="return-home" v-if="globalStore.lastRoute" icon="fa-solid fa-xmark" @click="router.push(globalStore.lastRoute)"/>
|
||||
</template>
|
||||
<script setup>
|
||||
import IconAction from '@/components/UI/IconAction.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
const globalStore = useGlobalStore();
|
||||
const router = useRouter();
|
||||
</script>
|
||||
<style scoped>
|
||||
.return-home {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.return-home:hover {
|
||||
color: var(--text-hover);
|
||||
}
|
||||
.icon {
|
||||
font-size: 1.3em;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
</style>
|
84
src/components/Features/ServerListItem.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<Box no-shadow level="second" padding="closed">
|
||||
<div class="container-list">
|
||||
<div class="container-avatar">
|
||||
<ServerAvatar :server-id="server.id" :server-icon="server.icon"/>
|
||||
<div class="info">
|
||||
<p class="name">{{ server.name }}</p>
|
||||
<p class="data"><Icon style="font-size: 10px;" :color="server.connected ? '#0BFF89' : '#FF0A0A'" icon="fa-solid fa-circle"/> {{ server.connected ? 'En ligne' : 'Hors ligne' }} - {{ server.serverMember }} membres</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="access()">Accéder</Button>
|
||||
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
<script setup>
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import ServerAvatar from '../UI/ServerAvatar.vue';
|
||||
import Button from '../UI/Button.vue';
|
||||
import IconAction from '../UI/IconAction.vue';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const props = defineProps({
|
||||
server: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
id: '',
|
||||
name: '',
|
||||
owner: false,
|
||||
icon: '',
|
||||
members: [],
|
||||
serverMember: 0,
|
||||
connected: false
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
function access() {
|
||||
router.push(`/servers/${props.server.id}`);
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.container-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.container-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.data {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
</style>
|
50
src/components/Features/UserAction.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<Box box-class="box" padding="closed">
|
||||
<User :user="userStore.userInfo" />
|
||||
<div class="user-action">
|
||||
<IconAction icon="fa-solid fa-gear" @click="goToSettings()"/>
|
||||
<IconAction color="red" icon="fa-solid fa-right-from-bracket" @click="signOut(router)"/>
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
</template>
|
||||
<script setup>
|
||||
import Box from '../UI/Box.vue';
|
||||
import IconAction from '../UI/IconAction.vue';
|
||||
import User from '../UI/User.vue';
|
||||
import { signOut } from '@/utils/UserRequest';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { socket } from '@/socket.js';
|
||||
|
||||
if(!socket.connected) {
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToSettings() {
|
||||
console.log(router)
|
||||
router.push("/settings");
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
gap: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
.user-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
</style>
|
37
src/components/Layout/DefaultSplash.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Splash>
|
||||
<Box :width="width" :style="`gap: ${props.gap};`" class="splash-box" box-class="splash-box">
|
||||
<slot></slot>
|
||||
</Box>
|
||||
</Splash>
|
||||
</template>
|
||||
<script setup>
|
||||
import Splash from '@/components/Layout/Splash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
|
||||
const props = defineProps({
|
||||
gap: {
|
||||
type: String,
|
||||
default: '10px'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.splash-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
44
src/components/Layout/Splash.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="splash-container">
|
||||
<div class="splash-title">
|
||||
<img src="/logo-white.svg" alt="Logo"/>
|
||||
<h1>Subsonics</h1>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.splash-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
gap: 46px;
|
||||
background-color: var(--main); /* Example background color */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Gunship';
|
||||
font-size: 30px;
|
||||
color: white;
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
}
|
||||
|
||||
.splash-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
23
src/components/UI/Avatar.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<img :src="`https://cdn.discordapp.com/avatars/${userId}/${avatarUrl}`" alt='User Avatar'>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
img {
|
||||
border-radius: 100%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
73
src/components/UI/Box.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div :style="widthStyle" :class="activeClass">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
div {
|
||||
background-color: var(--secondary);
|
||||
border-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.50);
|
||||
}
|
||||
|
||||
|
||||
.closed {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.far {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.first {
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
.second {
|
||||
background-color: var(--tertiary);
|
||||
}
|
||||
</style>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
level: {
|
||||
type: String,
|
||||
default: 'first'
|
||||
},
|
||||
padding: {
|
||||
type: String,
|
||||
|
||||
default: 'far'
|
||||
},
|
||||
boxClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const widthStyle = computed(() => {
|
||||
if (props.width) {
|
||||
return `max-width: ${props.width}; width: 70%;`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const activeClass = computed(() => {
|
||||
return `${props.level} ${props.padding} ${props.boxClass} ${props.noShadow ? '' : 'box-shadow'}`;
|
||||
});
|
||||
|
||||
|
||||
</script>
|
61
src/components/UI/Button.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<button :disabled="props.disabled" :class="disableClass" @click="$emit('click')">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
const events = defineEmits(['click']);
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const disableClass = computed(() => {
|
||||
return props.disabled ? 'disabled' : '';
|
||||
});
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
button {
|
||||
background-color: var(--main);
|
||||
color: rgb(255, 255, 255);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--main-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--main-active);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background-color: var(--tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disabled:hover {
|
||||
background-color: var(--tertiary) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disabled:active {
|
||||
background-color: var(--tertiary) !important;
|
||||
transform: none !important ;
|
||||
}
|
||||
</style>
|
30
src/components/UI/Error.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<Icon icon="fa-solid fa-circle-exclamation"></Icon>
|
||||
<div class="error-message">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-error);
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
text-align: justify;
|
||||
}
|
||||
</style>
|
36
src/components/UI/IconAction.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<Icon :color="color" :class="!fixed ? 'icon' : 'fixed'" :icon="icon" />
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--text)'
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.icon {
|
||||
font-size: 1.3em;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
font-size: 1.3em;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
</style>
|
30
src/components/UI/Info.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<Icon icon="fa-solid fa-circle-info"></Icon>
|
||||
<div class="info-message">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text);
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
display: flex;
|
||||
text-align: justify;
|
||||
}
|
||||
</style>
|
24
src/components/UI/ServerAvatar.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<img v-if="serverIcon" :src="`https://cdn.discordapp.com/icons/${serverId}/${serverIcon}.png?`" />
|
||||
<img v-else :src="`https://cdn.discordapp.com/embed/avatars/${serverId % 5}.png`" />
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
serverId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
serverIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
img {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
</style>
|
61
src/components/UI/User.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="user-card">
|
||||
<Avatar :avatar-url="user?.identity?.avatar" :user-id="user?.identity?.id" />
|
||||
<div class="user-info">
|
||||
<p class="global">{{ user.identity?.global_name || 'Nom d\'affichage inconnu' }}</p>
|
||||
<p class="username">{{ user.identity?.username || 'Identifiant inconnu' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
import Avatar from './Avatar.vue';
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
identity: {
|
||||
id: '261175887393718273',
|
||||
username: 'Identifiant inconnu',
|
||||
global_name: 'Nom d\'affichage inconnu',
|
||||
avatar: '96ecebdefe9fb9483b2e1b17a417ba32'
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
img {
|
||||
border-radius: 100%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
gap: 5px ;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.global {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
|
||||
}
|
||||
</style>
|
40
src/main.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* [Subsonics Chopin - Frontend] - Raphix - 07/2025
|
||||
* File: main.js
|
||||
* Description: Main entry point for the frontend application
|
||||
*/
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createHead } from '@unhead/vue/client'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons'
|
||||
import { far } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
import "@/assets/Global.scss"
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './routes'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
let app = createApp(App)
|
||||
let head = createHead({
|
||||
init: [{
|
||||
title: 'Subsonics',
|
||||
meta: [
|
||||
{ name: 'description', content: 'Interface de gestion du Bot Discord Subsonics' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ name: "charset", content: "utf-8" },
|
||||
]
|
||||
}]
|
||||
})
|
||||
let pinia = createPinia()
|
||||
|
||||
app.component('Icon', FontAwesomeIcon)
|
||||
library.add(fas, fab, far)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(head)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
100
src/routes.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import PageError from './views/PageError.vue'
|
||||
import Access from './utils/Access.vue'
|
||||
import Login from './views/Login.vue'
|
||||
import Privacy from './views/Privacy.vue'
|
||||
import Terms from './views/Terms.vue'
|
||||
import Redirect from './utils/Redirect.vue'
|
||||
import Servers from './views/Servers.vue'
|
||||
import { socket } from "@/socket"
|
||||
import { useLoginStore } from './stores/loginStore'
|
||||
import { useGlobalStore } from './stores/globalStore'
|
||||
import Interface from './views/Interface.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{path: "/", component: Access, alias: "/home"},
|
||||
{path: "/login", component: Login},
|
||||
{path: "/privacy", component: Privacy},
|
||||
{path: "/terms", component: Terms},
|
||||
{path: "/redirect", component: Redirect},
|
||||
{path: "/servers", component: Servers},
|
||||
{path: "/servers/:guildId", component: Interface, props: true},
|
||||
{path: "/:pathMatch(.*)*", component: PageError, props: {message: "404 - Page inexistante"}},
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
socket.removeAllListeners()
|
||||
console.log("Removing all socket listeners before navigation")
|
||||
applyListeners()
|
||||
console.log(`Navigation de ${from.path} vers ${to.path}`)
|
||||
const loginStore = useLoginStore()
|
||||
const globalStore = useGlobalStore()
|
||||
console.log(from.matched)
|
||||
if(from.matched.length == 0) {
|
||||
globalStore.setLastRoute(null)
|
||||
} else {
|
||||
globalStore.setLastRoute(from.path)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (to.path === "/") {
|
||||
if (loginStore.token) {
|
||||
next()
|
||||
} else {
|
||||
next("/login")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (to.path === "/servers") {
|
||||
if (loginStore.token) {
|
||||
next()
|
||||
} else {
|
||||
next("/login")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (to.path === "/login" && localStorage.getItem("token")) {
|
||||
next("/")
|
||||
return
|
||||
}
|
||||
|
||||
if (to.path === "/redirect" && localStorage.getItem("token")) {
|
||||
next("/")
|
||||
return
|
||||
}
|
||||
|
||||
if (to.path === "/home") {
|
||||
next("/")
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
function applyListeners() {
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`Connexion au serveur Subsonics réussi - ID : ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Déconnecté du serveur Subsonics");
|
||||
});
|
||||
|
||||
socket.on("connect_error", (error) => {
|
||||
console.error("Erreur de connexion au serveur Subsonics :", error);
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
console.error("Erreur du serveur Subsonics :", error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export default router
|
22
src/socket.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
var URL = null
|
||||
await fetch("/information.json").then(response => response.json())
|
||||
.then(data => {
|
||||
if (import.meta.env.DEV) {
|
||||
URL = data.backend.development;
|
||||
} else {
|
||||
URL = data.backend.production;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Erreur lors de la récupération de l'URL du backend :", error);
|
||||
});
|
||||
|
||||
export const socket = io(URL, {
|
||||
autoConnect: false,
|
||||
reconnectionAttempts: 0,
|
||||
auth: {
|
||||
token: localStorage.getItem("token") || null,
|
||||
sessionId: localStorage.getItem("session") || null
|
||||
},
|
||||
});
|
44
src/stores/globalStore.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
export const useGlobalStore = defineStore('global', () => {
|
||||
const isLoading = ref(true);
|
||||
const lastRoute = ref(null);
|
||||
const lastGuild = ref(localStorage.getItem("lastGuild") || null);
|
||||
const theme = ref(localStorage.getItem("theme") || "dark");
|
||||
document.documentElement.setAttribute("data-theme", theme.value);
|
||||
|
||||
function setLoading(loading) {
|
||||
console.log("Setting loading state to", loading);
|
||||
isLoading.value = loading;
|
||||
}
|
||||
|
||||
function setLastRoute(route) {
|
||||
console.log("Setting last route to", route);
|
||||
lastRoute.value = route;
|
||||
}
|
||||
|
||||
function setLastGuild(guild) {
|
||||
console.log("Setting last guild to", guild);
|
||||
lastGuild.value = guild;
|
||||
localStorage.setItem("lastGuild", guild || null);
|
||||
}
|
||||
|
||||
function toogleTheme() {
|
||||
theme.value = theme.value === "dark" ? "light" : "dark";
|
||||
localStorage.setItem("theme", theme.value);
|
||||
document.documentElement.setAttribute("data-theme", theme.value);
|
||||
console.log("Theme toggled to", theme.value);
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
setLoading,
|
||||
lastRoute,
|
||||
setLastRoute,
|
||||
theme,
|
||||
toogleTheme,
|
||||
lastGuild,
|
||||
setLastGuild
|
||||
};
|
||||
})
|
58
src/stores/loginStore.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
export const useLoginStore = defineStore('login',() =>{
|
||||
const session = ref(localStorage.getItem("session"));
|
||||
const token = ref(localStorage.getItem("token"));
|
||||
|
||||
watch(session, (newValue) => {
|
||||
if (newValue) {
|
||||
localStorage.setItem('session', newValue)
|
||||
} else {
|
||||
localStorage.removeItem('session')
|
||||
}
|
||||
})
|
||||
|
||||
watch(token, (newValue) => {
|
||||
if (newValue) {
|
||||
localStorage.setItem('token', newValue)
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
})
|
||||
|
||||
function setSession(newSession) {
|
||||
console.log("Setting new session");
|
||||
session.value = newSession;
|
||||
localStorage.setItem("session", newSession);
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
console.log("Clearing session");
|
||||
session.value = null;
|
||||
localStorage.removeItem("session");
|
||||
}
|
||||
|
||||
function setToken(newToken) {
|
||||
console.log("Setting new token");
|
||||
token.value = newToken;
|
||||
localStorage.setItem("token", newToken);
|
||||
|
||||
}
|
||||
function clear() {
|
||||
localStorage.removeItem("session");
|
||||
localStorage.removeItem("token");
|
||||
session.value = null;
|
||||
token.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
token,
|
||||
setSession,
|
||||
setToken,
|
||||
clear,
|
||||
clearSession
|
||||
}
|
||||
|
||||
})
|
30
src/stores/userStore.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref(null);
|
||||
|
||||
watch(userInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(newValue));
|
||||
} else {
|
||||
localStorage.removeItem('userInfo');
|
||||
}
|
||||
});
|
||||
|
||||
function setUserInfo(newUserInfo) {
|
||||
console.log("Setting new user info");
|
||||
userInfo.value = newUserInfo;
|
||||
}
|
||||
|
||||
function clearUserInfo() {
|
||||
console.log("Clearing user info");
|
||||
userInfo.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
setUserInfo,
|
||||
clearUserInfo,
|
||||
};
|
||||
})
|
24
src/utils/Access.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const router = useRouter();
|
||||
|
||||
console.log("Access Vue Loaded");
|
||||
|
||||
console.log(globalStore.lastGuild + "LAST GUILD FROM ACCESS VUE");
|
||||
if(!globalStore.lastGuild && globalStore.lastGuild !== null) {
|
||||
router.push("/servers");
|
||||
} else {
|
||||
router.push(`/servers/${globalStore.lastGuild}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
22
src/utils/IORequest.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { socket } from '@/socket';
|
||||
|
||||
export function IORequest(event, callback, data) {
|
||||
console.log(`IORequest: Emitting event ${event} with data:`, data);
|
||||
socket.emit(event, data);
|
||||
if (callback && typeof callback === 'function') {
|
||||
socket.once(event, (response) => {
|
||||
console.log(`IORequest: Received response for event ${event}:`, response);
|
||||
callback(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function IOListener(event, callback) {
|
||||
console.log(`IOListener: Listen for event ${event}`);
|
||||
socket.on(event, (data) => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
console.log(`IOListener: Received data for event ${event}:`, data);
|
||||
callback(data);
|
||||
}
|
||||
});
|
||||
}
|
40
src/utils/Loader.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
|
||||
const defaultMessage = "On s'accorde et on prépare le concert !";
|
||||
|
||||
const props = defineProps({
|
||||
interuptionMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<DefaultSplash>
|
||||
<h1 v-if="!interuptionMessage" class="separate"><Icon spin-pulse icon="fa-solid fa-spinner"/> Chargement de l'interface</h1>
|
||||
<h1 v-else><Icon icon="fa-solid fa-warning"/> Connexion interrompue</h1>
|
||||
<p v-if="interuptionMessage" class="retry"><Icon spin-pulse icon="fa-solid fa-spinner"/> Tentative de reconnexion en cours</p>
|
||||
<p v-if="interuptionMessage" class="error"><Icon icon="fa-solid fa-circle-xmark"/> {{ interuptionMessage }}</p>
|
||||
<p v-else>{{ defaultMessage }}</p>
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
<style scoped>
|
||||
.retry {
|
||||
font-size: 1.2em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.separate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
85
src/utils/Redirect.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<DefaultSplash width="600px;">
|
||||
|
||||
<h1><Icon spin-pulse icon="fa-solid fa-spinner"/> Connexion en cours</h1>
|
||||
<p>Vous allez être redirigé vers l'application.</p>
|
||||
<div class="info">
|
||||
<p class="second">Session : {{ loginStore.session }}</p>
|
||||
<p class="second">Auth_Code : {{ route.query.code }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import { useLoginStore } from '@/stores/loginStore';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {socket} from '@/socket.js';
|
||||
|
||||
const loginStore = useLoginStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
if(!loginStore.session) {
|
||||
router.push("/login?error=" + encodeURIComponent("Session expirée ou non valide"));
|
||||
}
|
||||
|
||||
if(!route.query.code) {
|
||||
if(route.query.error) {
|
||||
router.push("/login?error=" + encodeURIComponent("Connexion refusée par l'utilisateur"));
|
||||
} else {
|
||||
// If no error is specified, redirect to login with a generic error message
|
||||
router.push("/login?error=" + encodeURIComponent("Code d'authentification Discord manquant"));
|
||||
}
|
||||
} else {
|
||||
// Edit socket auth
|
||||
socket.auth = {
|
||||
sessionId: loginStore.session,
|
||||
auth_code: route.query.code
|
||||
};
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
socket.on("NEW_TOKEN", (token) => {
|
||||
loginStore.setToken(token);
|
||||
loginStore.setSession(null)
|
||||
router.push("/");
|
||||
});
|
||||
|
||||
|
||||
socket.on("error", () => {
|
||||
router.push("/login?error=" + encodeURIComponent("Déconnexion du serveur lors de la connexion"));
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
router.push("/login?error=" + encodeURIComponent("Erreur lors de l'authentification, veuillez réessayer"));
|
||||
});
|
||||
socket.on("connect_error", (error) => {
|
||||
router.push("/login?error=" + encodeURIComponent("Erreur de connexion au serveur : " + error.message));
|
||||
});
|
||||
|
||||
socket.on("NEW_SESSION", (data) => {
|
||||
console.log("Nouvelle session reçue :", data);
|
||||
loginStore.setSession(data);
|
||||
});
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.second {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: -10px;
|
||||
}
|
||||
</style>
|
88
src/utils/SocketEnvironment.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<Loader :interuption-message='interuptionMessage' v-if="globalStore.isLoading || interuptionMessage" />
|
||||
<slot v-else></slot>
|
||||
</template>
|
||||
<script setup>
|
||||
import Loader from '@/utils/Loader.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useLoginStore } from '@/stores/loginStore';
|
||||
import { socket } from '@/socket.js';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { IOListener, IORequest } from '@/utils/IORequest';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
|
||||
const router = useRouter();
|
||||
const interuptionMessage = ref(null);
|
||||
const needReload = ref(true)
|
||||
|
||||
const loginStore = useLoginStore();
|
||||
const userStore = useUserStore();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
loginStore.clearSession();
|
||||
socket.auth.sessionId = null;
|
||||
socket.auth.token = loginStore.token;
|
||||
|
||||
if(!socket.connected) {
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
IOListener("/USER/READY", () => {
|
||||
IORequest("/USER/INFO", (data) => {
|
||||
interuptionMessage.value = null;
|
||||
userStore.setUserInfo(data);
|
||||
console.log("User info received:", data);
|
||||
globalStore.setLoading(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
IOListener("AUTH_ERROR", (error) => {
|
||||
console.error("Authentication error:", error);
|
||||
loginStore.setToken(null);
|
||||
globalStore.setLoading(true)
|
||||
router.push("/login?error=" + encodeURIComponent("Erreur d'authentification : " + error));
|
||||
socket.removeAllListeners();
|
||||
needReload.value = false
|
||||
})
|
||||
|
||||
socket.on("connect", () => {
|
||||
interuptionMessage.value = null;
|
||||
globalStore.setLoading(true);
|
||||
});
|
||||
|
||||
socket.on("connect_error", (error) => {
|
||||
interuptionMessage.value = "Erreur de connexion au serveur : " + error.message;
|
||||
tryReconnect();
|
||||
});
|
||||
|
||||
socket.on("error", () => {
|
||||
interuptionMessage.value = "Erreur de connexion au serveur, veuillez réessayer plus tard";
|
||||
tryReconnect();
|
||||
})
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
interuptionMessage.value = "Déconnecté du serveur";
|
||||
tryReconnect();
|
||||
})
|
||||
|
||||
|
||||
function tryReconnect() {
|
||||
|
||||
if(socket.connected) {
|
||||
interuptionMessage.value = null;
|
||||
return;
|
||||
}
|
||||
setInterval(() => {
|
||||
console.log("Tentative de reconnexion au serveur...");
|
||||
if(!socket.connected || !needReload.value) {
|
||||
socket.auth.token = loginStore.token;
|
||||
socket.connect();
|
||||
} else {
|
||||
clearInterval(this);
|
||||
}
|
||||
}, 3000); // Essai de reconnexion après 5 secondes
|
||||
}
|
||||
</script>r
|
11
src/utils/UserRequest.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useLoginStore } from "@/stores/loginStore";
|
||||
import { IORequest } from "@/utils/IORequest";
|
||||
|
||||
export function signOut(router) {
|
||||
const loginStore = useLoginStore();
|
||||
|
||||
IORequest("/USER/SIGNOUT", () => {
|
||||
loginStore.setToken(null);
|
||||
router.push("/login?message=" + encodeURIComponent("Vous avez été déconnecté"));
|
||||
})
|
||||
}
|
95
src/views/Interface.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<SocketEnvironment>
|
||||
<div class="container">
|
||||
<Box>
|
||||
<Button @click="router.push('/servers')">Choisir un serveur</Button>
|
||||
<Button @click="router.push('/terms')">Terms</Button>
|
||||
<Button @click="router.push('/privacy')">Privacy</Button>
|
||||
<Button @click="globalStore.toogleTheme()">Changer le thème</Button>
|
||||
<p>{{ guildId }}</p>
|
||||
</Box>
|
||||
<UserAction/>
|
||||
</div>
|
||||
</SocketEnvironment>
|
||||
</template>
|
||||
<script setup>
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import SocketEnvironment from '@/utils/SocketEnvironment.vue';
|
||||
import UserAction from '@/components/Features/UserAction.vue';
|
||||
import { watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { IOListener } from '@/utils/IORequest';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
guildId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const guildId = props.guildId;
|
||||
if (!guildId) {
|
||||
globalStore.setLastGuild(null);
|
||||
router.push('/servers');
|
||||
}
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
watch(() => globalStore.isLoading, (value) => {
|
||||
if(!value) {
|
||||
loadInteface();
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if(globalStore.lastGuild && !globalStore.isLoading) {
|
||||
loadInteface();
|
||||
}
|
||||
});
|
||||
|
||||
function loadInteface() {
|
||||
console.log("Loading interface")
|
||||
console.log("Guild ID:", guildId);
|
||||
checkGuildAvailability();
|
||||
}
|
||||
|
||||
function checkGuildAvailability() {
|
||||
if(userStore.userInfo.guilds.filter(guild => guild.id === guildId).length === 0) {
|
||||
globalStore.setLastGuild(null);
|
||||
console.warn("Guild not found, redirecting to servers list.");
|
||||
router.push('/servers');
|
||||
} else {
|
||||
globalStore.setLastGuild(guildId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px;;
|
||||
max-width: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
193
src/views/Login.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<DefaultSplash width="500px;" gap="30px">
|
||||
<h1>Votre ticket ?</h1>
|
||||
<p>Connexion par Discord (obligatoire)</p>
|
||||
<Button :disabled='!loginReady' @click="handleLogin()"><Icon v-if="loading" spin-pulse icon="fa-solid fa-spinner"/><img src="/discord-logo-white.png"></Button>
|
||||
<div v-if="error || errorServer || callbackError || message" class="error-container">
|
||||
<Error v-if="callbackError">{{ callbackError }}</Error>
|
||||
<Error v-if="error">{{ error }}</Error>
|
||||
<Error v-if="errorServer">{{ errorServer }}</Error>
|
||||
<Info v-if="message">{{ message }}</Info>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p class="secondtext"><router-link to="/terms">Conditions d'utilisation</router-link> </p>
|
||||
<p class="secondtext"><router-link to="/privacy">Privacy</router-link></p>
|
||||
</div>
|
||||
<p class="second">Session : {{ sessionId }}</p>
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import Error from '@/components/UI/Error.vue';
|
||||
import Info from '@/components/UI/Info.vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useLoginStore } from '@/stores/loginStore';
|
||||
import { computed, ref, watch, onMounted } from 'vue';
|
||||
import { socket } from '@/socket.js';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
|
||||
|
||||
const loginStore = useLoginStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
callbackError: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const callbackError = ref(undefined);
|
||||
const message = ref(undefined);
|
||||
const errorServer = ref(undefined);
|
||||
const error = ref(undefined);
|
||||
const serverError = ref(true);
|
||||
const loading = ref(true);
|
||||
const sessionId = computed(() => loginStore.session ? loginStore.session : "Aucune session active");
|
||||
const loginReady = computed(() => loginStore.session && discordUrl.value && !serverError.value);
|
||||
var discordUrl = ref(null);
|
||||
|
||||
if(socket.connected) {
|
||||
socket.disconnect()
|
||||
}
|
||||
socket.connect();
|
||||
|
||||
if(props.callbackError) {
|
||||
callbackError.value = props.callbackError;
|
||||
}
|
||||
if(route.query.error) {
|
||||
callbackError.value = route.query.error;
|
||||
}
|
||||
|
||||
if(route.query.message) {
|
||||
message.value = route.query.message;
|
||||
}
|
||||
|
||||
if(!globalStore.lastRoute) {
|
||||
callbackError.value = null;
|
||||
message.value = null;
|
||||
error.value = null;
|
||||
errorServer.value = null;
|
||||
}
|
||||
|
||||
var errorSessionMsg = "Aucune session n'est active. Le serveur est-il en ligne ?";
|
||||
|
||||
watch(loginReady, (ready) => {
|
||||
if(ready) {
|
||||
loading.value = false;
|
||||
} else {
|
||||
loading.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
if(!loginStore.session) {
|
||||
error.value = errorSessionMsg;
|
||||
}
|
||||
watch(() => loginStore.session, (session) => {
|
||||
error.value = !session ? errorSessionMsg: undefined;
|
||||
});
|
||||
|
||||
fetch("/information.json")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
discordUrl.value = import.meta.env.DEV ? data.discord.development : data.discord.production;
|
||||
|
||||
})
|
||||
.catch(e => {
|
||||
console.error("Error fetching Discord URL:", e);
|
||||
error.value = "Une erreur est survenue lors de la récupération de l'URL Discord.";
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
|
||||
function handleLogin() {
|
||||
window.location.href = discordUrl.value;
|
||||
}
|
||||
|
||||
socket.on("NEW_SESSION", (data) => {
|
||||
console.log("Nouvelle session reçue :", data);
|
||||
loginStore.setSession(data);
|
||||
socket.io.opts.reconnection = false;
|
||||
socket.disconnect();
|
||||
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
serverError.value = false;
|
||||
if(loginStore.token) {
|
||||
window.location.href = discordUrl.value;
|
||||
}
|
||||
console.log("Login : Socket connected successfully");
|
||||
});
|
||||
|
||||
socket.on("connect_error", (error) => {
|
||||
loading.value = false;
|
||||
errorServer.value = "Erreur de connexion au serveur. Veuillez réessayer plus tard.";
|
||||
error.value = "Erreur de connexion au serveur. Veuillez réessayer plus tard.";
|
||||
serverError.value = true;
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
img {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 !important;
|
||||
font-size: 25px;
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 160px;;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 100vh;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.secondtext {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 10px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.second {
|
||||
color: var(--text-secondary);
|
||||
font-size: 8px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
</style>
|
45
src/views/PageError.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<DefaultSplash width="600px">
|
||||
<div class="error-page">
|
||||
<h1>Erreur ! </h1>
|
||||
<strong><p>On s'est trompé de mesure !</p></strong>
|
||||
<Box no-shadow level="second" padding="closed">
|
||||
<p>Raison : {{ message }}</p>
|
||||
</Box>
|
||||
|
||||
</div>
|
||||
<br>
|
||||
<router-link class="no-decoration" to="/">
|
||||
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
|
||||
</router-link>
|
||||
</DefaultSplash>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import Button from '@/components/UI/Button.vue'
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import { socket } from '@/socket';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: "Une erreur est survenue"
|
||||
}
|
||||
})
|
||||
|
||||
if(socket.connected) {
|
||||
socket.disconnect();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.error-page {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;;
|
||||
}
|
||||
|
||||
</style>
|
119
src/views/Privacy.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup>
|
||||
import ReturnHomeButton from '@/components/Actions/ReturnHomeButton.vue';
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultSplash>
|
||||
<ReturnHomeButton/>
|
||||
<h1>Privacy</h1>
|
||||
<Box no-shadow level="second" padding="closed">
|
||||
<div class="terms-content">
|
||||
<p><strong>Date d'entrée en vigueur :</strong> 22 juillet 2025</p>
|
||||
<p><strong>Responsable du traitement des données :</strong> Raphix (<code>raphixscrap</code> sur Discord)</p>
|
||||
|
||||
<h2>1. Introduction</h2>
|
||||
<p>Subsonics (le "bot" et l’"application") est un bot Discord et une application Web proposant des fonctionnalités audio pour enrichir l’expérience de vos serveurs. </p>
|
||||
<p>Cette politique de confidentialité décrit quelles données sont collectées, comment elles sont utilisées, et quels sont vos droits.</p>
|
||||
<p>En utilisant Subsonics ou son interface Web, vous acceptez les conditions de cette politique de confidentialité.</p>
|
||||
|
||||
<h2>2. Données collectées</h2>
|
||||
<p><strong>Données collectées automatiquement (pour le bon fonctionnement) :</strong></p>
|
||||
<ul>
|
||||
<li>Identifiant utilisateur Discord (User ID)</li>
|
||||
<li>Identifiant de serveur (Guild ID)</li>
|
||||
<li>Identifiant de salon (Channel ID)</li>
|
||||
<li>Commandes utilisées et journaux d’exécution</li>
|
||||
<li>Données de lecture audio (titres de morceaux, horodatage, etc.)</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Concernant l’application Web :</strong></p>
|
||||
<ul>
|
||||
<li>Adresse IP (logs de sécurité)</li>
|
||||
<li>Informations du navigateur (User-Agent)</li>
|
||||
<li>Cookies (préférences, sécurité, analytics si activé)</li>
|
||||
</ul>
|
||||
|
||||
<p>Nous <strong>ne collectons pas</strong> vos messages privés, vos conversations vocales ni aucune donnée personnelle sensible.</p>
|
||||
|
||||
<h2>3. Utilisation des données</h2>
|
||||
<p>Les données collectées sont utilisées uniquement pour :</p>
|
||||
<ul>
|
||||
<li>Assurer le bon fonctionnement du bot</li>
|
||||
<li>Corriger les bugs et améliorer l’expérience utilisateur</li>
|
||||
<li>Analyser l’utilisation de certaines commandes (statistiques anonymes)</li>
|
||||
<li>Empêcher les abus et sécuriser l’infrastructure</li>
|
||||
</ul>
|
||||
<p>Nous ne revendons ni ne partageons vos données avec des tiers.</p>
|
||||
|
||||
<h2>4. Conservation des données</h2>
|
||||
<ul>
|
||||
<li>Les données des utilisateurs et serveurs sont conservées <strong>tant que le bot est présent sur le serveur et tant qu'une demande de supression n'a été expressément demandé</strong>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Vos droits</h2>
|
||||
<p>Conformément au RGPD, vous avez le droit de :</p>
|
||||
<ul>
|
||||
<li>Demander l’accès à vos données</li>
|
||||
<li>Demander la suppression de vos données</li>
|
||||
<li>Demander la rectification d’informations incorrectes</li>
|
||||
<li>Retirer votre consentement à tout moment (en retirant le bot de votre serveur ou via une demande)</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Suppression des données / Demandes</h2>
|
||||
<p>Pour toute demande liée à vos données personnelles, vous pouvez contacter le développeur :</p>
|
||||
<ul>
|
||||
<li><strong>Nom :</strong> Raphix</li>
|
||||
<li><strong>Contact Discord :</strong> <code>raphixscrap</code></li>
|
||||
<li><strong>Informations à fournir :</strong> votre identifiant Discord (User ID) ou l’ID du serveur concerné</li>
|
||||
</ul>
|
||||
|
||||
<h2>7. Sécurité</h2>
|
||||
<p>Nous mettons en œuvre des mesures techniques et organisationnelles raisonnables pour protéger vos données, incluant :</p>
|
||||
<ul>
|
||||
<li>Accès limité aux données</li>
|
||||
<li>Stockage sécurisé</li>
|
||||
<li>Mises à jour régulières des dépendances</li>
|
||||
<li>Surveillance et alertes en cas d’anomalie</li>
|
||||
</ul>
|
||||
<p>Malgré cela, aucun système n’est totalement invulnérable et nous ne pouvons garantir une sécurité absolue.</p>
|
||||
|
||||
<h2>8. Services tiers</h2>
|
||||
<p>Subsonics utilise l’API Discord ainsi que des services d’hébergement. Ces plateformes ont leurs propres politiques de confidentialité :</p>
|
||||
<p><a href="https://discord.com/privacy" target="_blank">Politique de confidentialité de Discord</a></p>
|
||||
|
||||
<h2>9. Modifications de cette politique</h2>
|
||||
<p>Cette politique peut être mise à jour à tout moment. N'hésitez pas à consulter régulièrement cette page pour rester informé des changements.</p>
|
||||
|
||||
<h2>10. Contact</h2>
|
||||
<p>Pour toute question, suggestion ou réclamation :</p>
|
||||
<ul>
|
||||
<li><strong>Développeur :</strong> Raphix</li>
|
||||
<li><strong>Contact Discord :</strong> <code>raphixscrap</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
<br/>
|
||||
<router-link class="no-decoration" to="/">
|
||||
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
|
||||
</router-link>
|
||||
<!-- Add more content here as needed -->
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
<style scoped>
|
||||
.terms-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 35vh;
|
||||
overflow-y: auto;
|
||||
color: var(--text-secondary);
|
||||
text-align: justify;
|
||||
}
|
||||
</style>
|
117
src/views/Servers.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import UserAction from '@/components/Features/UserAction.vue';
|
||||
import Splash from '@/components/Layout/Splash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import SocketEnvironment from "../utils/SocketEnvironment.vue"
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import ReturnHomeButton from '@/components/Actions/ReturnHomeButton.vue';
|
||||
import { ref } from 'vue';
|
||||
import ServerListItem from '@/components/Features/ServerListItem.vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
console.log("Last route:", globalStore.lastRoute);
|
||||
const router = useRouter();
|
||||
const hasLink = ref(false);
|
||||
const botInviteUrl = ref("");
|
||||
|
||||
console.log(globalStore.lastGuild);
|
||||
|
||||
fetch('/information.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
botInviteUrl.value = import.meta.env.DEV ? data.bot_invite.development : data.bot_invite.production;
|
||||
hasLink.value = true
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching bot invite URL:", error);
|
||||
hasLink.value = false;
|
||||
});
|
||||
|
||||
function inviteSubsonics() {
|
||||
|
||||
window.open(botInviteUrl.value, '_blank');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SocketEnvironment>
|
||||
<Splash>
|
||||
<ReturnHomeButton v-if="globalStore.lastGuild"/>
|
||||
<div class="servers-div">
|
||||
<Box box-class="server-box" padding="closed">
|
||||
<div class="servers-content">
|
||||
<div class="servers-container">
|
||||
<div class="servers-list">
|
||||
<ServerListItem v-for="server in userStore.userInfo.guilds" :key="server.id" :server="server" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button :disabled="!hasLink" @click="inviteSubsonics()"><Icon icon="fa-solid fa-user-plus"/>Inviter Subsonics</Button>
|
||||
</div>
|
||||
</Box>
|
||||
<UserAction/>
|
||||
</div>
|
||||
</Splash>
|
||||
</SocketEnvironment>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.servers-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 90%;
|
||||
max-width: 800px;;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.servers-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
|
||||
}
|
||||
|
||||
.server-box {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
|
||||
}
|
||||
|
||||
.servers-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap:10px;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.servers-container {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
max-height: 45vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;;
|
||||
}
|
||||
|
||||
</style>
|
43
src/views/Terms.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import ReturnHomeButton from '@/components/Actions/ReturnHomeButton.vue';
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultSplash>
|
||||
<ReturnHomeButton/>
|
||||
<h1>Conditions d'utilisation</h1>
|
||||
<Box no-shadow level="second" padding="closed">
|
||||
<div class="terms-content">
|
||||
<p>En utilisant ce site, vous consentez à l'utilisation de vos données fournis par Discord <Icon icon="fa-solid fa-copyright"/> afin d'assurer le bon fonctionnement de la plateforme. Vous avez la possibilité de demander la suppression de vos données à tout moment en contactant Raphix (raphixscrap). Tout acte intentionnel de dégradation du bot entraînera un bannissement du site.</p>
|
||||
<p>Ce bot est destiné à un usage privé exclusivement et n'est pas conçu pour être utilisé par le grand public.</p>
|
||||
<p>Toutes les musiques disponibles sur ce site sont hébergées sur les plateformes suivantes : Youtube, Soundcloud, Spotify.</p>
|
||||
<p>En utilisant Subsonics, vous bénéficiez du droit d'accès au service et de l'écoute du contenu. Cependant, veuillez noter que Raphix n'est pas tenu de fournir la provenance ni l'autorisation d'exploitation des musiques par les ayants droits. Il vous incombe donc d'obtenir les autorisations nécessaires des ayants droits pour écouter le contenu.</p>
|
||||
<p>En utilisant Subsonics, vous acceptez de ne pas utiliser le bot pour diffuser des contenus illégaux, violents, haineux, discriminatoires, ou à caractère sexuel.</p>
|
||||
<p>En utilisant Subsonics, vous acceptez de ne pas utiliser le bot pour diffuser des contenus à caractère politique, religieux, incitant à la haine, publicitaire ou pornographique.</p>
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
<br>
|
||||
<router-link class="no-decoration" to="/">
|
||||
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
|
||||
</router-link>
|
||||
<!-- Add more content here as needed -->
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
<style scoped>
|
||||
.terms-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 35vh;
|
||||
overflow-y: auto;
|
||||
color: var(--text-secondary);
|
||||
text-align: justify;
|
||||
}
|
||||
</style>
|
14
vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, 'src'),
|
||||
"@public": path.resolve(__dirname, 'public'),
|
||||
}
|
||||
}
|
||||
})
|