Version 1.0.0 - Finalisation de Account et de GuildHeader
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"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"
|
||||
"development": "https://discord.com/oauth2/authorize?client_id=1342913183744004158&response_type=code&redirect_uri=http%3A%2F%2F192.168.1.77%3A8080%2Fredirect&scope=identify",
|
||||
"production": "https://discord.com/oauth2/authorize?client_id=1094727789682380922&response_type=code&redirect_uri=https%3A%2F%2Fsubsonics.raphix.fr%2Fredirect&scope=identify"
|
||||
},
|
||||
"backend": {
|
||||
"development": "http://192.168.1.77:3000",
|
||||
|
@@ -2,7 +2,6 @@
|
||||
<router-view/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
const globalStore = useGlobalStore();
|
||||
console.log("Subsonics Chopin - App Vue Loaded");
|
||||
|
@@ -4,33 +4,44 @@
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--main: #CD034F;
|
||||
--main-hover: #A0023F;
|
||||
--main-active: #7A002F;
|
||||
|
||||
|
||||
--primary: #111210;
|
||||
--primary-hover: #ececec;
|
||||
--secondary: #2A2B28;
|
||||
--tertiary: #404040;
|
||||
--text: #FFFFFF;
|
||||
--text-inverse: #111210;
|
||||
--text-secondary: #C5c3c3;
|
||||
--text-tertiary: #A5A5A5;
|
||||
--text-error: #ff2b2b;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
|
||||
--primary: #FFFFFF;
|
||||
--primary-hover: #292b26;
|
||||
--secondary: #EAEAEA;
|
||||
--tertiary: #cacaca;
|
||||
--text: #111210;
|
||||
--text-inverse: #FFFFFF;
|
||||
--text-secondary: #404040;
|
||||
--text-tertiary: #C5c3c3;
|
||||
--text-error: #CD034F;
|
||||
}
|
||||
|
||||
:root {
|
||||
--main: #CD034F;
|
||||
--main-hover: #A0023F;
|
||||
--main-active: #7A002F;
|
||||
|
||||
--primary: #FFFFFF;
|
||||
--secondary: #EAEAEA;
|
||||
--tertiary: #C5c3c3;
|
||||
--text: #111210;
|
||||
--text-secondary: #404040;
|
||||
--text-error: #CD034F;
|
||||
--text-success: #00ff00;
|
||||
|
||||
color: var(--text);
|
||||
--admin-color: #209AFE;
|
||||
--owner-color: #FFAA32;
|
||||
--mod-color: #0BFF89;
|
||||
}
|
||||
|
||||
html, body {
|
||||
@@ -43,7 +54,9 @@ html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--primary);
|
||||
transition: all 0.5s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
color: var(--text);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -86,3 +99,53 @@ a {
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
background-color: var(--tertiary);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
outline: none;
|
||||
min-height: 4vh;
|
||||
max-height: 15vh;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
box-shadow: 0 0 5px var(--main);
|
||||
}
|
||||
|
||||
|
||||
@keyframes appear {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes unfold {
|
||||
from {
|
||||
max-height: 0px;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
max-height: 100vh;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
background-color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--main-hover) !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:hover {
|
||||
background-color: var(--main-active) !important;
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<Box padding="closed">
|
||||
<div class="container">
|
||||
<SubsonicsLogo/>
|
||||
<div class="server-box">
|
||||
<Box ref="collapsedBoxRef" level="second" padding="closed">
|
||||
<div v-if="server" class="itm">
|
||||
<ServerItem :server="server"/>
|
||||
<div class="actions">
|
||||
<ListenBox>{{ server.members.length + 1}}</ListenBox>
|
||||
<IconAction @click="showMenu = !showMenu" :icon="showMenu ? 'fa-solid fa-angle-up' : 'fa-solid fa-angle-down'"/>
|
||||
</div>
|
||||
</div>
|
||||
<Error v-else><ServerItem/></Error>
|
||||
<div v-if="showMenu">
|
||||
<p>Other items</p>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
<script setup>
|
||||
import SubsonicsLogo from '@/components/UI/SubsonicsLogo.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import ServerItem from '@/components/Widget/ServerItem.vue';
|
||||
import IconAction from '../UI/IconAction.vue';
|
||||
import ListenBox from '@/components/UI/ListenBox.vue';
|
||||
import Error from '@/components/UI/Error.vue';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import events from '@/utils/Events';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const server = ref(undefined)
|
||||
const showMenu = ref(false);
|
||||
|
||||
|
||||
|
||||
events.on("UPDATE", () => {
|
||||
updateServerInfo();
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateServerInfo();
|
||||
})
|
||||
|
||||
function updateServerInfo() {
|
||||
IORequest("/GUILD/INFO", (data) => {
|
||||
server.value = data;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;;
|
||||
}
|
||||
|
||||
.itm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-box {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.collapsed-box {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
</style>
|
@@ -2,11 +2,13 @@
|
||||
<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)"/>
|
||||
<IconAction title="Paramètres" icon="fa-solid fa-gear" @click="userSettings.open()"/>
|
||||
<IconAction title="Déconnexion" color="red" icon="fa-solid fa-right-from-bracket" @click="signOut(router)"/>
|
||||
</div>
|
||||
|
||||
<UserSettings ref="userSettings"/>
|
||||
</Box>
|
||||
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import Box from '../UI/Box.vue';
|
||||
@@ -21,15 +23,14 @@ if(!socket.connected) {
|
||||
}
|
||||
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import Modal from '../UI/Modal.vue';
|
||||
import UserSettings from '../Widget/User/UserSettings.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToSettings() {
|
||||
console.log(router)
|
||||
router.push("/settings");
|
||||
}
|
||||
const userSettings = ref(null);
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
170
src/components/Layout/GuildHeader.vue
Normal file
170
src/components/Layout/GuildHeader.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<Box padding="closed">
|
||||
<div class="container">
|
||||
<SubsonicsLogo/>
|
||||
<div ref="serverBox" class="server-box">
|
||||
<Box :overbox="showMenu" level="second" no-shadow padding="closed">
|
||||
<div v-if="server" class="itm">
|
||||
<ServerItem :server="server"/>
|
||||
<div class="actions">
|
||||
<ListenBox>{{ server.members.length + 1}}</ListenBox>
|
||||
<IconAction @click="showMenu = !showMenu" :icon="showMenu ? 'fa-solid fa-angle-up' : 'fa-solid fa-angle-down'"/>
|
||||
</div>
|
||||
</div>
|
||||
<Error v-else><ServerItem/></Error>
|
||||
</Box>
|
||||
<div :style="`width: ${serverBox.offsetWidth}px !important;`" class="menu" v-if="showMenu">
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<p><Icon icon="fa-solid fa-users"/> Utilisateurs en ligne</p>
|
||||
<div v-if="server" class="users-container">
|
||||
<div v-if="server.members.length > 0" class="users-list">
|
||||
<GuildHeaderUsers :server="server"/>
|
||||
</div>
|
||||
<Info secondary v-else>Aucun utilisateur en ligne</Info>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-actions">
|
||||
<Button v-if="gestion" @click="settings.open()"><Icon icon="fa-solid fa-gear"/>Gestion</Button>
|
||||
<Button :style="{width: gestion ? '' : '100%'}" @click="router.push(`/servers`)"><Icon icon="fa-solid fa-arrow-right"/>Changer de serveur</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showMenu" style="height: 10px;"></div>
|
||||
<GuildSettings ref="settings" :server="server" v-if="server && gestion"/>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
<script setup>
|
||||
import SubsonicsLogo from '@/components/UI/SubsonicsLogo.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import ServerItem from '@/components/UI/Server.vue';
|
||||
import IconAction from '../UI/IconAction.vue';
|
||||
import ListenBox from '@/components/UI/ListenBox.vue';
|
||||
import Error from '@/components/UI/Error.vue';
|
||||
import Info from '@/components/UI/Info.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import events from '@/utils/Events';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import GuildHeaderUsers from '../Widget/Guild/GuildHeaderUsers.vue';
|
||||
import GuildSettings from '../Widget/Guild/GuildSettings.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const server = ref(undefined)
|
||||
const showMenu = ref(false);
|
||||
const serverBox = ref(null);
|
||||
const router = useRouter();
|
||||
|
||||
var gestion = ref(false)
|
||||
|
||||
const settings = ref(null);
|
||||
|
||||
events.on("UPDATE", () => {
|
||||
updateServerInfo();
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateServerInfo();
|
||||
window.addEventListener('resize', () => {
|
||||
showMenu.value = false;
|
||||
});
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {
|
||||
showMenu.value = false;
|
||||
});
|
||||
userStore.userInfo.identity.isAdmin = null;
|
||||
userStore.userInfo.identity.isMod = null;
|
||||
userStore.userInfo.identity.isOwner = null;
|
||||
});
|
||||
|
||||
function updateServerInfo() {
|
||||
IORequest("/GUILD/INFO", (data) => {
|
||||
server.value = data;
|
||||
userStore.userInfo.identity.isAdmin = userStore.userInfo.labels.includes('ADMIN');
|
||||
userStore.userInfo.identity.isMod = userStore.userInfo.labels.includes('MOD_' + server.value.id);
|
||||
userStore.userInfo.identity.isOwner = userStore.userInfo.identity.id == server.value.owner;
|
||||
gestion.value = userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.labels.includes('MOD_' + server.value.id) || userStore.userInfo.identity.id == server.value.owner;
|
||||
console.log("Server info updated");
|
||||
events.emit("GUILD_JOINED");
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;;
|
||||
}
|
||||
|
||||
.itm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-box {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.collapsed-box {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.server-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.server-actions p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: absolute;
|
||||
background-color: var(--tertiary);
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
padding: 0px 10px 10px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.users-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-height: 20vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
35
src/components/Layout/Search.vue
Normal file
35
src/components/Layout/Search.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<input type="text" placeholder="Search..." v-model="searchQuery" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const searchQuery = ref('');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: var(--tertiary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.search input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.search input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<img :src="`https://cdn.discordapp.com/avatars/${userId}/${avatarUrl}`" alt='User Avatar'>
|
||||
<Icon v-if="tag === 'admin'" style="color: var(--main);" icon="fa-solid fa-star" class="tag" />
|
||||
<Icon v-if="tag === 'owner'" style="color: #FFAA32;" icon="fa-solid fa-crown" class="tag" />
|
||||
<Icon v-if="tag === 'mod'" style="color: #0BFF89;" icon="fa-solid fa-shield-halved" class="tag" />
|
||||
<Icon v-if="tag === 'admin'" style="color: var(--admin-color);" icon="fa-solid fa-star" class="tag" />
|
||||
<Icon v-if="tag === 'owner'" style="color: var(--owner-color);" icon="fa-solid fa-crown" class="tag" />
|
||||
<Icon v-if="tag === 'mod'" style="color: var(--mod-color);" icon="fa-solid fa-shield-halved" class="tag" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :style="widthStyle" :class="activeClass">
|
||||
<div :style="widthStyle + overboxStyle" :class="activeClass">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,8 +7,6 @@
|
||||
div {
|
||||
background-color: var(--secondary);
|
||||
border-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
@@ -55,6 +53,10 @@ const props = defineProps({
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overbox: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -65,6 +67,10 @@ const widthStyle = computed(() => {
|
||||
return '';
|
||||
});
|
||||
|
||||
const overboxStyle = computed(() => {
|
||||
return props.overbox ? 'border-radius: 10px 10px 0 0; padding: 10px 10px 0 10px;' : '';
|
||||
});
|
||||
|
||||
const activeClass = computed(() => {
|
||||
return `${props.level} ${props.padding} ${props.boxClass} ${props.noShadow ? '' : 'box-shadow'}`;
|
||||
});
|
||||
|
@@ -10,11 +10,15 @@ const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
colorLower: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const disableClass = computed(() => {
|
||||
return props.disabled ? 'disabled' : '';
|
||||
return props.disabled ? props.colorLower ? 'disabled color-lower' : 'disabled' : '';
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -24,13 +28,14 @@ button {
|
||||
color: rgb(255, 255, 255);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
padding: 7px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
}
|
||||
|
||||
button:hover {
|
||||
@@ -58,4 +63,16 @@ button:active {
|
||||
background-color: var(--tertiary) !important;
|
||||
transform: none !important ;
|
||||
}
|
||||
|
||||
.color-lower {
|
||||
background-color: var(--secondary) !important;
|
||||
}
|
||||
|
||||
.color-lower:hover {
|
||||
background-color: var(--secondary) !important;
|
||||
}
|
||||
|
||||
.color-lower:active {
|
||||
background-color: var(--secondary) !important;
|
||||
}
|
||||
</style>
|
113
src/components/UI/ContextMenu.vue
Normal file
113
src/components/UI/ContextMenu.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div ref="contextMenu" class="contextmenu">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import useMouse from '@/utils/Mouse';
|
||||
|
||||
const contextMenu = ref(null);
|
||||
const mouse = useMouse();
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
contextMenu.value.addEventListener('rightclick', (e) => {
|
||||
e.preventDefault(); // Prevent default context menu
|
||||
});
|
||||
|
||||
contextMenu.value.addEventListener('click', () => {
|
||||
contextMenu.value.style.display = 'none'; // Hide context menu on click
|
||||
});
|
||||
|
||||
contextMenu.value.addEventListener('mouseleave', () => {
|
||||
contextMenu.value.style.display = 'none'; // Hide context menu on mouse leave
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function show() {
|
||||
const menu = contextMenu.value
|
||||
const { x, y } = mouse.value
|
||||
|
||||
// Affiche temporairement pour mesurer
|
||||
menu.style.display = 'flex'
|
||||
menu.style.left = '0px'
|
||||
menu.style.top = '0px'
|
||||
|
||||
// Force le DOM à calculer les dimensions
|
||||
const menuRect = menu.getBoundingClientRect()
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
|
||||
let left = x
|
||||
let top = y
|
||||
|
||||
// Ajuste si dépasse à droite
|
||||
if (x + menuRect.width > windowWidth) {
|
||||
left = windowWidth - menuRect.width
|
||||
} else {
|
||||
left = x - 5
|
||||
}
|
||||
|
||||
// Ajuste si dépasse en bas
|
||||
if (y + menuRect.height > windowHeight) {
|
||||
top = windowHeight - menuRect.height
|
||||
} else {
|
||||
top = y - 5
|
||||
}
|
||||
|
||||
// Applique la position corrigée
|
||||
menu.style.left = `${Math.max(left, 0)}px`
|
||||
menu.style.top = `${Math.max(top, 0)}px`
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.contextmenu {
|
||||
position: fixed;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
z-index: 1000;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
:deep(div) {
|
||||
background-color: var(--primary);
|
||||
opacity: 0.9;
|
||||
width: 100%;
|
||||
gap: 10px;;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
padding: 0 7px;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
:deep(div:first-child) {
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
}
|
||||
|
||||
:deep(div:last-child) {
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
}
|
||||
|
||||
:deep(div:only-child) {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:deep(div:hover) {
|
||||
background-color: var(--primary-hover);
|
||||
color: var(--text-inverse);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
@@ -2,11 +2,12 @@
|
||||
<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 Splash from '@/components/UI/Splash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
|
||||
const props = defineProps({
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Icon :color="color" :class="!fixed ? 'icon' : 'fixed'" :icon="icon" />
|
||||
<Icon :style="{'font-size': size ? size : ''}" :color="color" :class="!fixed ? 'icon' : 'fixed'" :icon="icon" />
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
@@ -14,6 +14,10 @@ const props = defineProps({
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="{ second: secondary }">
|
||||
<Icon icon="fa-solid fa-circle-info"></Icon>
|
||||
<div class="info-message">
|
||||
<slot></slot>
|
||||
@@ -8,6 +8,14 @@
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps({
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -19,7 +27,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text);
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -27,4 +34,9 @@
|
||||
display: flex;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.second {
|
||||
color: var(--text-secondary) !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
24
src/components/UI/Logo.vue
Normal file
24
src/components/UI/Logo.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<LogoDark class="img" v-if="globalStore.theme == 'light'"/>
|
||||
<LogoLight class="img" v-else/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import LogoDark from '@/assets/LogoDark.vue';
|
||||
import LogoLight from '@/assets/LogoLight.vue';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
<style scoped>
|
||||
.img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
146
src/components/UI/Modal.vue
Normal file
146
src/components/UI/Modal.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div ref="modal" class="modal-overlay">
|
||||
<div ref="modalContent" class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<Icon font-size="1.5em" v-if="icon" :icon="icon" />
|
||||
<p>{{ title }}</p>
|
||||
</div>
|
||||
<IconAction @click="close()" icon="fa-solid fa-xmark" />
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import IconAction from '../UI/IconAction.vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import Events from '@/utils/Events';
|
||||
|
||||
const modal = ref(null);
|
||||
const modalContent = ref(null);
|
||||
|
||||
// Close the modal when clicking outside of it
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
// Only clicking modal-overlay but not modal-content will close the modal
|
||||
function handleClickOutside(event) {
|
||||
if (event.target === modal.value && modalContent.value && !modalContent.value.contains(event.target)) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Modal Title'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function close() {
|
||||
if (modal.value) {
|
||||
modal.value.style.display = 'none';
|
||||
}
|
||||
Events.emit('modal:close');
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (modal.value) {
|
||||
modal.value.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close
|
||||
});
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.modal-content {
|
||||
padding: 0 20px 20px 20px;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background-color: var(--primary);
|
||||
border-radius: 10px 10px 0px 0px ;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--secondary);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
max-width: 600px !important;
|
||||
width: 100%;
|
||||
|
||||
max-width: 90%;
|
||||
margin-right: 15px;
|
||||
margin-left: 15px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
animation: appear 0.2s ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
}
|
||||
.modal p {
|
||||
margin: 0px;
|
||||
font-weight: 600;
|
||||
font-size: 1.2em;
|
||||
font-family: 'Gunship', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1500;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
45
src/components/UI/ModalTree.vue
Normal file
45
src/components/UI/ModalTree.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<Icon style="width: 20px;" class="settings-icon" :icon="icon"/>
|
||||
<div class="settings-item">
|
||||
<p class="settings-title">{{ title }}</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Settings item'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'fa-solid fa-cog'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.settings-title {
|
||||
font-size: 1.1em;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
23
src/components/UI/Role.vue
Normal file
23
src/components/UI/Role.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<Tag color="var(--admin-color)" v-if="user.identity.isAdmin">Administrateur</Tag>
|
||||
<Tag color="var(--owner-color)" v-else-if="user.identity.isOwner">Propriétaire</Tag>
|
||||
<Tag color="var(--mod-color)" v-else-if="user.identity.isMod">Modérateur</Tag>
|
||||
<Tag color="var(--text-secondary)" v-else>Membre</Tag>
|
||||
</template>
|
||||
<script setup>
|
||||
import Tag from '@/components/UI/Tag.vue';
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: {
|
||||
identity: {
|
||||
isAdmin: false,
|
||||
isMod: false,
|
||||
isOwner: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
163
src/components/UI/Selector.vue
Normal file
163
src/components/UI/Selector.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import IconAction from './IconAction.vue';
|
||||
import { ref, onMounted, watch, onUnmounted } from 'vue';
|
||||
import { useSlots } from 'vue';
|
||||
|
||||
const slots = useSlots();
|
||||
const boxWidth = ref(0);
|
||||
const box = ref(null);
|
||||
|
||||
const allSlots = slots.default ? slots.default() : [];
|
||||
|
||||
var firstSlot = allSlots[0] || null;
|
||||
var otherSlots = allSlots.slice(1);
|
||||
|
||||
const showMenu = ref(false);
|
||||
|
||||
function selectOption(index) {
|
||||
// Make it like a select
|
||||
if (otherSlots[index]) {
|
||||
const oldFirstSlot = firstSlot;
|
||||
firstSlot = otherSlots[index];
|
||||
otherSlots.splice(index, 1);
|
||||
otherSlots.push(oldFirstSlot);
|
||||
showMenu.value = false; // Hide the menu after selection
|
||||
}
|
||||
updateModelValue(firstSlot ? firstSlot.props.value : '');
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function updateModelValue(value) {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
window.addEventListener('rightclick', (e) => {
|
||||
e.preventDefault(); // Prevent default context menu
|
||||
});
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
hideMenu(e);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
showMenu.value = false;
|
||||
|
||||
});
|
||||
|
||||
updateModelValue(firstSlot ? firstSlot.props.value : '');
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('rightclick', (e) => {
|
||||
e.preventDefault(); // Prevent default context menu
|
||||
});
|
||||
window.removeEventListener('click', (e) => {
|
||||
hideMenu(e);
|
||||
});
|
||||
|
||||
window.removeEventListener('resize', () => {
|
||||
showMenu.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
function hideMenu(e) {
|
||||
// Check if the click is outside the context menu
|
||||
if (box.value && !box.value.contains(e.target)) {
|
||||
showMenu.value = false; // Hide context menu on click outside
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
firstSlot: () => {
|
||||
return firstSlot;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<div ref="box" :class="showMenu ? `showed firstbox` : 'firstbox'" @click="showMenu = !showMenu">
|
||||
<template v-if="firstSlot">
|
||||
<component :is="firstSlot" />
|
||||
</template>
|
||||
<IconAction
|
||||
:icon="showMenu ? 'fa-solid fa-angle-up' : 'fa-solid fa-angle-down'"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="otherSlots.length && showMenu">
|
||||
<div class="container">
|
||||
<div v-for="(slot, index) in otherSlots" :style="`width: ${box.offsetWidth - 10}px;`" class="option" @click="selectOption(index)" >
|
||||
<component :is="slot" :key="index" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.firstbox {
|
||||
background-color: var(--tertiary);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
max-height: 16vh;
|
||||
}
|
||||
|
||||
.container .option:last-child {
|
||||
border-radius: 0px 0px 5px 5px !important;
|
||||
}
|
||||
|
||||
.container .option:only-child {
|
||||
border-radius: 0px 0px 5px 5px !important;
|
||||
}
|
||||
|
||||
.container .option:first-child {
|
||||
border-top: 1px solid var(--secondary) !important;
|
||||
}
|
||||
|
||||
.option {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
background-color: var(--tertiary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: var(--primary-hover);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.showed {
|
||||
/* margin-bottom: 0 !important;
|
||||
padding-bottom: 0; */
|
||||
border-radius: 5px 5px 0 0 !important;
|
||||
}
|
||||
</style>
|
30
src/components/UI/Success.vue
Normal file
30
src/components/UI/Success.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<Icon icon="fa-solid fa-check-circle"></Icon>
|
||||
<div class="success-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-success);
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
text-align: justify;
|
||||
}
|
||||
</style>
|
33
src/components/UI/Tag.vue
Normal file
33
src/components/UI/Tag.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="tag" :style="{ borderColor: props.color }">
|
||||
<Icon :color="props.color" icon="fa-circle fa-solid"/>
|
||||
<p><slot></slot></p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.tag {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<div class="user-card">
|
||||
<Avatar :avatar-url="user?.identity?.avatar" :user-id="user?.identity?.id" />
|
||||
<Avatar :avatar-url="user?.identity?.avatar" :user-id="user?.identity?.id"
|
||||
:isMod="user?.identity?.isMod"
|
||||
:isOwner="user?.identity?.isOwner"
|
||||
:isAdmin="user?.identity?.isAdmin"/>
|
||||
<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>
|
||||
<p class="global">{{ user?.identity?.global_name || 'Nom d\'affichage inconnu' }}</p>
|
||||
<p class="username">{{ user?.identity?.username || 'Identifiant inconnu' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
73
src/components/Widget/Guild/GuildHeaderUsers.vue
Normal file
73
src/components/Widget/Guild/GuildHeaderUsers.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="container" v-for="(member, index) in server.members" :key="index">
|
||||
<User :user="{identity: member}" />
|
||||
<IconAction :title="`Action sur ${member.username}`" @click="setTargetUser(member)" v-if="(userStore.userInfo.labels.includes('ADMIN') || (userStore.userInfo.labels.includes('MOD_' + server.id) && !member.isMod) || server.owner == userStore.userInfo.identity.id) && (member.id != server.owner && !member.isAdmin) "
|
||||
icon="fa-solid fa-ellipsis-vertical"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<ContextMenu ref="menuRef" v-if="userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.labels.includes('MOD_' + server.id) || server.owner == userStore.userInfo.identity.id">
|
||||
<div v-if="!targetUser?.isMod" @click="banUser()">
|
||||
<Icon icon="fa-solid fa-hammer"/>
|
||||
<p>Bannir</p>
|
||||
</div>
|
||||
<div @click="toogleMod()" v-if="userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.identity.id == server.owner">
|
||||
<Icon icon="fa-solid fa-user-shield"/>
|
||||
<p v-if="!targetUser?.isMod">Nommer modérateur</p>
|
||||
<p v-else>Retirer les droits de modérateur</p>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import User from '@/components/UI/User.vue';
|
||||
import { ref } from 'vue';
|
||||
import IconAction from '@/components/UI/IconAction.vue';
|
||||
import ContextMenu from '@/components/UI/ContextMenu.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import Events from '@/utils/Events';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const targetUser = ref(null);
|
||||
const menuRef = ref(null);
|
||||
|
||||
function setTargetUser(user) {
|
||||
targetUser.value = user;
|
||||
if (menuRef.value && targetUser.value) {
|
||||
menuRef.value.show();
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
server: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function toogleMod() {
|
||||
if (!targetUser.value) return;
|
||||
IORequest("/OWNER/USERS/SWITCH_MOD", () => {
|
||||
console.log("Mod switched for user", targetUser.value.id);
|
||||
}, targetUser.value.id)
|
||||
}
|
||||
|
||||
function banUser() {
|
||||
// : /MOD/USERS/BAN
|
||||
if (!targetUser.value) return;
|
||||
IORequest("/MOD/USERS/BAN", () => {
|
||||
console.log("User banned:", targetUser.value.id);
|
||||
}, targetUser.value.id);
|
||||
//TODO: CHECK THE userListConnected which have sameUser
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
42
src/components/Widget/Guild/GuildSettings.vue
Normal file
42
src/components/Widget/Guild/GuildSettings.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<Modal ref="modal" icon="fa-solid fa-screwdriver-wrench" :title="`Gestion : ${server?.name}`">
|
||||
<ModalTree title="Utilisateurs" icon="fa-solid fa-users">
|
||||
<GuildUsers :server="server"/>
|
||||
</ModalTree>
|
||||
<ModalTree title="Statistiques" icon="fa-solid fa-chart-simple">
|
||||
<GuildStats :server="server"/>
|
||||
|
||||
</ModalTree>
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import Modal from '@/components/UI/Modal.vue';
|
||||
import ModalTree from '@/components/UI/ModalTree.vue';
|
||||
import { ref } from 'vue';
|
||||
import GuildUsers from './Settings/GuildUsers.vue';
|
||||
import GuildStats from './Settings/GuildStats.vue';
|
||||
import Events from '@/utils/Events';
|
||||
|
||||
const modal = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
server: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
open() {
|
||||
if (modal.value) {
|
||||
modal.value.open();
|
||||
Events.emit("GUILD_JOINED")
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (modal.value) {
|
||||
modal.value.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
128
src/components/Widget/Guild/Settings/GuildStats.vue
Normal file
128
src/components/Widget/Guild/Settings/GuildStats.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="metrics" v-if="metrics">
|
||||
<div class="metric" v-for="metric in metrics" :key="metric.name">
|
||||
<div class="metric-header">
|
||||
<Icon class="metric-icon" :icon="getIcons(metric.name)" />
|
||||
<div class="metric-info">
|
||||
<p class="metric-name">{{ metric.description.replace(server.id, "").replace(server.name, "").replace(":", "") }}</p>
|
||||
<p class="metric-id">{{ metric.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="metric-value" v-if="metric.name.includes('Seconds')">{{ getReadableDuration(metric.value) }}</p>
|
||||
<p v-else class="metric-value">{{ metric.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="second">Aucune statistique enregistrée !</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import Events from '@/utils/Events';
|
||||
import { ref } from 'vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { getReadableDuration } from '@/utils/TimeConverter';
|
||||
|
||||
function getIcons(name) {
|
||||
if(name.includes("Commands")) return "fa-solid fa-terminal";
|
||||
if(name.includes("Music")) return "fa-solid fa-music";
|
||||
if(name.includes("Seconds")) return "fa-solid fa-clock";
|
||||
return "fa-solid fa-chart-bar";
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const metrics = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
server: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
Events.on("GUILD_JOINED", () => {
|
||||
if(!(userStore.userInfo.identity.isAdmin || userStore.userInfo.identity.isMod || userStore.userInfo.identity.id == props.server.owner)) return;
|
||||
IORequest("/MOD/STATS", (data) => {
|
||||
metrics.value = data;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.second {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: var(--tertiary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-info p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.metric {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.metric-name {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.metric-id {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.7em;
|
||||
word-break: break-all;;
|
||||
}
|
||||
|
||||
.metric-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
background-color: var(--secondary);
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
background-color: var(--secondary);
|
||||
padding: 20px;
|
||||
border-radius: 100%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
139
src/components/Widget/Guild/Settings/GuildUsers.vue
Normal file
139
src/components/Widget/Guild/Settings/GuildUsers.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="users">
|
||||
<div class="user" v-for="member in users" :key="member.identity.id">
|
||||
<div class="group">
|
||||
<User :user="member" />
|
||||
<div class="info">
|
||||
<Role :user="member" />
|
||||
<Tag v-if="member.isBanned" color="var(--text-error)">Banni</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="member.identity.id == userStore.userInfo.identity.id">
|
||||
<p class="you">Vous</p>
|
||||
</div>
|
||||
<div v-else-if="member.identity.isAdmin">
|
||||
<p class="you">Administrateur</p>
|
||||
</div>
|
||||
<div v-else-if="member.identity.isOwner">
|
||||
<p class="you">Propriétaire</p>
|
||||
</div>
|
||||
<div class="actions" v-else>
|
||||
<IconAction title="Bannir" v-if="!member.identity.isMod" :color="member.isBanned ? 'var(--text)' : 'var(--text-tertiary)'" @click="banUser(member)" icon="fa-solid fa-hammer"/>
|
||||
<IconAction title="Nommer / Retirer modérateur" v-if="userStore.userInfo.identity.id == server.owner && !member.isBanned" :color="member.identity.isMod ? 'var(--text)' : 'var(--text-tertiary)'" @click="toggleMod(member)" icon="fa-solid fa-user-shield"/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Events from '@/utils/Events';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import { defineProps } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import User from '@/components/UI/User.vue';
|
||||
import Role from '@/components/UI/Role.vue';
|
||||
import IconAction from '@/components/UI/IconAction.vue';
|
||||
import Tag from '@/components/UI/Tag.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
|
||||
const users = ref(null)
|
||||
const userStore = useUserStore();
|
||||
|
||||
const props = defineProps({
|
||||
server: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
Events.on("GUILD_JOINED", () => {
|
||||
if(!(userStore.userInfo.identity.isAdmin || userStore.userInfo.identity.isMod || userStore.userInfo.identity.id == props.server.owner)) return;
|
||||
IORequest("/MOD/USERS/LIST", (data) => {
|
||||
users.value = data;
|
||||
});
|
||||
});
|
||||
|
||||
function toggleMod(member) {
|
||||
if(member.identity.id == userStore.userInfo.identity.id) return;
|
||||
IORequest("/OWNER/USERS/SWITCH_MOD", (response) => {
|
||||
console.log(response)
|
||||
}, member.identity.id);
|
||||
}
|
||||
|
||||
function banUser(member) {
|
||||
if(member.identity.id == userStore.userInfo.identity.id) return;
|
||||
IORequest("/MOD/USERS/BAN", (response) => {
|
||||
console.log(response)
|
||||
}, member.identity.id);
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 20px;
|
||||
|
||||
}
|
||||
|
||||
.user{
|
||||
padding: 5px 10px 5px 5px !important;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user{
|
||||
background-color: var(--tertiary);
|
||||
padding: 5px 10px 2px 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.users {
|
||||
margin: 10px 0;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: 25vh;
|
||||
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.you {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,21 +1,28 @@
|
||||
<template>
|
||||
<Box no-shadow level="second" padding="closed">
|
||||
<div class="container-list">
|
||||
<ServerItem :server="server"/>
|
||||
<Server :server="server"/>
|
||||
<ServerOnlinePicture class="sop" :key="server.id" v-if="server.members.length > 0" :members="server.members"/>
|
||||
<Button class="btn" @click="access()">Accéder</Button>
|
||||
<section>
|
||||
<Error v-if="userStore.userInfo.labels.includes('BAN_' + server.id)">Banni</Error>
|
||||
<Button color-lower :disabled="userStore.userInfo.labels.includes('BAN_' + server.id)" class="btn" @click="access()"><Icon icon="fa-solid fa-right-to-bracket"/>Accéder</Button>
|
||||
</section>
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
<script setup>
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import Button from '../UI/Button.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import ServerOnlinePicture from '@/components/Widget/ServerOnlinePicture.vue';
|
||||
import ServerItem from '@/components/Widget/ServerItem.vue';
|
||||
import ServerOnlinePicture from '@/components/Widget/Server/ServerOnlinePicture.vue';
|
||||
import Server from '@/components/UI/Server.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import Error from '@/components/UI/Error.vue';
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const props = defineProps({
|
||||
@@ -61,6 +68,12 @@ function access() {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-list {
|
||||
@@ -70,6 +83,10 @@ function access() {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.sop {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -77,6 +94,11 @@ function access() {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
@@ -15,8 +15,9 @@
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import Avatar from '../UI/Avatar.vue';
|
||||
import Circle from '../UI/Circle.vue';
|
||||
import Avatar from '@/components/UI/Avatar.vue';
|
||||
import Circle from '@/components/UI/Circle.vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
members: {
|
||||
@@ -26,8 +27,13 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
// ne garder que les quatres premiers membres
|
||||
const length = props.members.length;
|
||||
const members = props.members.slice(0, 4);
|
||||
const length = ref(props.members.length);
|
||||
const members = ref(props.members.slice(0, 4));
|
||||
|
||||
watch(() => props.members, (newMembers) => {
|
||||
members.value = newMembers.slice(0, 4);
|
||||
length.value = newMembers.length;
|
||||
}, { immediate: true });
|
||||
|
||||
</script>
|
||||
<style scoped>
|
100
src/components/Widget/User/Settings/BugReport.vue
Normal file
100
src/components/Widget/User/Settings/BugReport.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import Selector from '@/components/UI/Selector.vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import Success from '@/components/UI/Success.vue';
|
||||
import Events from '@/utils/Events.js';
|
||||
|
||||
const reportDesc = ref(null)
|
||||
const category = ref(null); // Default category
|
||||
const sended = ref(false);
|
||||
const selector = ref(null);
|
||||
|
||||
const showForm = ref(false);
|
||||
|
||||
function sendReport() {
|
||||
if (reportDesc.value && category.value) {
|
||||
IORequest('/REPORT', (answer) => {
|
||||
if(answer) {
|
||||
sended.value = true;
|
||||
reportDesc.value = null;
|
||||
category.value = null; // Reset category after sending
|
||||
showForm.value = false; // Hide the form after sending
|
||||
}
|
||||
}, {
|
||||
level: category.value,
|
||||
desc: selector.value.firstSlot().children + " - " + reportDesc.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(reportDesc, (newValue) => {
|
||||
if (newValue) {
|
||||
sended.value = false; // Reset sent status when description changes
|
||||
}
|
||||
});
|
||||
|
||||
Events.on('modal:close', () => {
|
||||
showForm.value = false; // Close the form when modal is closed
|
||||
reportDesc.value = null; // Reset the description
|
||||
category.value = null; // Reset the category
|
||||
sended.value = false; // Reset sent status
|
||||
});
|
||||
|
||||
watch(showForm, (newValue) => {
|
||||
if (newValue) {
|
||||
reportDesc.value = null; // Reset description when form is shown
|
||||
category.value = null; // Reset category when form is shown
|
||||
sended.value = false; // Reset sent status when form is shown
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>Si vous rencontrez un problème, vous pouvez le signaler via ce formulaire. Vous pouvez être contacté en cas de besoin par Raphix pour plus d'informations.</p>
|
||||
<Button v-if="!showForm" @click="showForm = !showForm" class="margin"><Icon icon="fa-solid fa-paper-plane" /> Faire un rapport de bug</Button>
|
||||
<Success class="margin" v-if="sended">Votre rapport a été envoyé avec succès</Success>
|
||||
<div v-if="showForm" class="report-content">
|
||||
<p>Catégorie</p>
|
||||
<Selector ref="selector" v-model="category">
|
||||
<option value="bug">Problème d'authentification (Déconnexion intempestive, Serveurs)</option>
|
||||
<option value="bug">Problème avec l'interface (Affichage)</option>
|
||||
<option value="bug">Problème avec le Bot Discord (Réponse vide, Erreur)</option>
|
||||
<option value="bug">Problème avec le Player (Musique)</option>
|
||||
<option value="bug">Problème de fonctionnalité (Non spécifié)</option>
|
||||
<option value="suggestion">Suggestion</option>
|
||||
</Selector>
|
||||
<p>Description</p>
|
||||
<textarea v-model="reportDesc" name="report-desc" class="report-desc"></textarea>
|
||||
<Button @click="sendReport()" :disabled="!reportDesc" id="report-send-confirm" class="report-send">Envoyer</Button>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-secondary);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
animation: unfold 1.3s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.report-content p {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
</style>
|
104
src/components/Widget/User/Settings/InformationSettings.vue
Normal file
104
src/components/Widget/User/Settings/InformationSettings.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="info">
|
||||
<p class="name">Chopin - Fait avec <IconAction @click="modal.open()" size="10px" color="var(--text-secondary)" icon="fa-solid fa-heart"/> par Raphix</p>
|
||||
<div class="version">
|
||||
<p>Frontend: {{ versionFrontend }}</p>
|
||||
<p>Backend: {{ versionBackend }}</p>
|
||||
</div>
|
||||
<Modal title="Merci" icon="fa-solid fa-heart" ref="modal">
|
||||
<div class="thanks">
|
||||
<Logo/>
|
||||
</div>
|
||||
<div class="text">
|
||||
<p>Subsonics a été développé avec ❤️ par Raphix et existe depuis le 9 avril 2023.</p>
|
||||
<p>❤️ Merci énormément à tous le CLP en particulier : ❤️</p>
|
||||
<ul>
|
||||
<li>IcePlayer: Pour ses conseils, son "RAAAPHIX", sa confiance accordée et pour avoir créer le CLP</li>
|
||||
<li>Gabouille : Pour ses conseils, sa bonne humeur, pour le magnifique logo et pour m'avoir aider à redesigner le site.</li>
|
||||
<li>Roman: Pour ses conseils de musique mais aussi de vie, sa créativité et sa musique plus qu'excellente !</li>
|
||||
<li>Mido : Pour son soutien plus que légendaire et d'être à mes côtés en toute circonstance ! (il a même l'application Subsonics !!!)</li>
|
||||
<li>Immudelki : Pour ses conseils de vie, sa sagesse et d'avoir fait un petit jeu d'interface qui a bercé mon enfance</li>
|
||||
<li>Alexmario : Pour sa compagnie et son talent indéniable et indétroné sur tout ce qui touche !</li>
|
||||
<li>Ava : Pour ses conseils de vie, sa sagesse et d'avoir toujours été là pour moi !</li>
|
||||
<li>DarkGuillaume : Pour sa créativité, et ... SHADOW !!!!!</li>
|
||||
<li>Mako : Pour son humour, sa bonne humeur et d'être le saint patron des mouettes !</li>
|
||||
<li>Et à tous les autres qui ont contribué à ce projet, que ce soit par des conseils, des idées ou simplement en étant là !</li>
|
||||
</ul>
|
||||
<p>Un petit peu d'histoire sur Subsonics :</p>
|
||||
<ul>
|
||||
<li>Première version nommée "Bot", accompagné de son application "Manager", ayant marqué les prémices de Subsonics.</li>
|
||||
<li>Deuxième version nommée "Web", avec une interface Web et l'ajout de beaucoup de fonctionnalités.</li>
|
||||
<li>Troisième version nommée "Chopin", avec une refonte complète de l'interface, une architecture améliorée et une expérience utilisateur optimisée.</li>
|
||||
</ul>
|
||||
<p>Merci d'utiliser Subsonics ^^ !</p>
|
||||
<p>Raphaël.P dit "Raphix"❤️</p>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import IconAction from '@/components/UI/IconAction.vue';
|
||||
import { version as versionFrontend } from '@/../package.json';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import Modal from '@/components/UI/Modal.vue';
|
||||
import Logo from '@/components/UI/Logo.vue';
|
||||
import Avatar from '@/components/UI/Avatar.vue';
|
||||
|
||||
const modal = ref(null);
|
||||
|
||||
const versionBackend = ref("X.X.X");
|
||||
|
||||
onMounted(() => {
|
||||
IORequest("/VERSION", (data) => {
|
||||
versionBackend.value = data;
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-self: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 5px 0 0 0 ;
|
||||
}
|
||||
|
||||
.version p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.thanks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
101
src/components/Widget/User/Settings/PrivacySettings.vue
Normal file
101
src/components/Widget/User/Settings/PrivacySettings.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<p class="privacy">Toutes les données récupérées sont à des fins <strong>strictement</strong> nécessaires au bon fonctionnement de l'application.</p>
|
||||
|
||||
<Button @click="openModal()"><Icon icon="fa-solid fa-trash"/> Supprimer mon compte</Button>
|
||||
<Modal icon="fa-solid fa-trash" ref="deleteAccountModal" title="Supprimer mon compte">
|
||||
<p class="warning">
|
||||
Êtes-vous sûr de vouloir supprimer votre compte ? <br/>
|
||||
Cette action est irréversible et supprime vos playlists, votre historique, vos rôles ! <br/>
|
||||
<span class="text-second">Cependant, en vertu de la politique de confidentialité, votre identifiant Discord sera conservé ainsi que les informations générées par Subsonics.</span>
|
||||
</p>
|
||||
<div @click="deleteAccount = !deleteAccount" class="options">
|
||||
<input v-model="deleteAccount" type="checkbox" id="deleteAccount"/>
|
||||
<p class="text-second">Je suis sur de vouloir supprimer mon compte et toutes les données associées.</p>
|
||||
</div>
|
||||
<Button @click="closeModal()">Annuler</Button>
|
||||
<Button :disabled="!deleteAccount" @click="deleteAcc()">Confirmer la suppression</Button>
|
||||
</Modal>
|
||||
<div class="privacy-link">
|
||||
<p class="secondtext"><router-link to="/terms">Conditions d'utilisation</router-link> </p>
|
||||
<p class="secondtext"><router-link to="/privacy">Confidentialité</router-link></p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import Events from '@/utils/Events';
|
||||
import { ref } from 'vue';
|
||||
import Modal from '@/components/UI/Modal.vue';
|
||||
import { IORequest } from '@/utils/IORequest';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { useLoginStore } from '@/stores/loginStore';
|
||||
|
||||
const deleteAccount = ref(false);
|
||||
const deleteAccountModal = ref(null);
|
||||
const loginStore = useLoginStore();
|
||||
const router = useRouter();
|
||||
|
||||
Events.on('modal:close', () => {
|
||||
deleteAccount.value = false; // Reset checkbox when modal is closed
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
deleteAccountModal.value.open()
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
deleteAccountModal.value.close();
|
||||
deleteAccount.value = false; // Reset checkbox when modal is closed
|
||||
}
|
||||
|
||||
function deleteAcc() {
|
||||
if (deleteAccount.value) {
|
||||
IORequest('/USER/DELETE', () => {
|
||||
loginStore.setToken(null); // Clear the token
|
||||
router.push("/login?message=" + encodeURIComponent("Votre compte a été supprimé avec succès."));
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.privacy {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
.privacy-link {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.secondtext {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.text-second {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
101
src/components/Widget/User/Settings/ThemeSelector.vue
Normal file
101
src/components/Widget/User/Settings/ThemeSelector.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="theme-selector">
|
||||
<div @click="lightTheme.click()" class="theme-option">
|
||||
<div>
|
||||
<input ref="lightTheme" type="radio" name="theme" value="light" v-model="selectedTheme" />
|
||||
<p>Clair</p>
|
||||
</div>
|
||||
</div>
|
||||
<div @click="darkTheme.click()" class="theme-option">
|
||||
<div>
|
||||
<input ref="darkTheme" type="radio" name="theme" value="dark" v-model="selectedTheme" />
|
||||
<p>Sombre</p>
|
||||
</div>
|
||||
</div>
|
||||
<div @click="systemThemeInput.click()" class="theme-option">
|
||||
<div>
|
||||
<input ref="systemThemeInput" type="radio" name="theme" value="system" v-model="selectedTheme" />
|
||||
<p>Système ({{ systemTheme }})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { ref, watch } from 'vue';
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const selectedTheme = ref(localStorage.getItem('theme') || 'dark');
|
||||
const systemTheme = ref(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'Sombre' : 'Clair');
|
||||
|
||||
const lightTheme = ref(null);
|
||||
const darkTheme = ref(null);
|
||||
const systemThemeInput = ref(null);
|
||||
|
||||
watch(() => selectedTheme.value, (newTheme) => {
|
||||
globalStore.setTheme(newTheme);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.theme-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
.theme-selector div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.theme-selector span {
|
||||
font-size: 1em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.theme-selector input[type="radio"]:checked + span {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.theme-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;;
|
||||
gap: 20px !important;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
.theme-option:hover {
|
||||
transition: backgroundColor 0s ease-in-out;
|
||||
background-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-selector p {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.theme-samples {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin: 0 !important;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
</style>
|
47
src/components/Widget/User/UserSettings.vue
Normal file
47
src/components/Widget/User/UserSettings.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<Modal ref="modal" icon="fa-solid fa-gear" title="Paramètres">
|
||||
<ModalTree title="Thème" icon="fa-solid fa-palette">
|
||||
<ThemeSelector />
|
||||
</ModalTree>
|
||||
<ModalTree title="Support" icon="fa-solid fa-headset">
|
||||
<BugReport/>
|
||||
</ModalTree>
|
||||
<ModalTree title="Mon compte & Confidentialité" icon="fa-solid fa-shield-halved">
|
||||
<PrivacySettings />
|
||||
</ModalTree>
|
||||
<ModalTree title="Informations" icon="fa-solid fa-circle-info">
|
||||
<InformationSettings/>
|
||||
</ModalTree>
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import Modal from '@/components/UI/Modal.vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import ThemeSelector from '@/components/Widget/User/Settings/ThemeSelector.vue';
|
||||
import ModalTree from '@/components/UI/ModalTree.vue';
|
||||
import BugReport from './Settings/BugReport.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import PrivacySettings from './Settings/PrivacySettings.vue';
|
||||
import InformationSettings from './Settings/InformationSettings.vue';
|
||||
|
||||
const modal = ref(null);
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
|
||||
defineExpose({
|
||||
open() {
|
||||
if (modal.value) {
|
||||
modal.value.open();
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (modal.value) {
|
||||
modal.value.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -19,4 +19,5 @@ export const socket = io(URL, {
|
||||
token: localStorage.getItem("token") || null,
|
||||
sessionId: localStorage.getItem("session") || null
|
||||
},
|
||||
|
||||
});
|
@@ -5,7 +5,11 @@ 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");
|
||||
const theme = ref(localStorage.getItem("theme") || "system");
|
||||
if(theme.value === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
theme.value = systemTheme;
|
||||
}
|
||||
document.documentElement.setAttribute("data-theme", theme.value);
|
||||
|
||||
function setLoading(loading) {
|
||||
@@ -24,6 +28,17 @@ export const useGlobalStore = defineStore('global', () => {
|
||||
localStorage.setItem("lastGuild", guild || null);
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
console.log("Setting theme to", newTheme);
|
||||
localStorage.setItem("theme", newTheme);
|
||||
theme.value = newTheme;
|
||||
if(theme.value === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
theme.value = systemTheme;
|
||||
}
|
||||
document.documentElement.setAttribute("data-theme", theme.value);
|
||||
}
|
||||
|
||||
function toogleTheme() {
|
||||
theme.value = theme.value === "dark" ? "light" : "dark";
|
||||
localStorage.setItem("theme", theme.value);
|
||||
@@ -39,6 +54,7 @@ export const useGlobalStore = defineStore('global', () => {
|
||||
theme,
|
||||
toogleTheme,
|
||||
lastGuild,
|
||||
setLastGuild
|
||||
setLastGuild,
|
||||
setTheme
|
||||
};
|
||||
})
|
@@ -14,6 +14,7 @@ export default {
|
||||
},
|
||||
emit(eventName, data) {
|
||||
if(Events.has(eventName)) {
|
||||
console.log(`Event emitted: ${eventName}`, data)
|
||||
Events.get(eventName).forEach(fn => fn(data))
|
||||
}
|
||||
},
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
|
||||
|
||||
const defaultMessage = "On s'accorde et on prépare le concert !";
|
||||
const connectMsg = "Erreur de connexion au serveur : xhr poll error"
|
||||
@@ -16,11 +16,11 @@ const props = defineProps({
|
||||
<template>
|
||||
<DefaultSplash>
|
||||
<h1 v-if="!interuptionMessage" class="separate"><Icon spin-pulse icon="fa-solid fa-spinner"/> Chargement de l'interface</h1>
|
||||
<h1 v-else-if="interuptionMessage == connectMsg">Echec de connexion</h1>
|
||||
<h1 v-else-if="interuptionMessage == connectMsg"><Icon icon="fa-solid fa-circle-exclamation"/> Echec de connexion</h1>
|
||||
<h1 v-else><Icon icon="fa-solid fa-warning"/> Connexion interrompue</h1>
|
||||
<div class="separate-col">
|
||||
<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="second">Contactez Raphix (raphixscrap) en cas de problème prolongé.</p>
|
||||
<p v-if="interuptionMessage" class="second">Si vous rencontrez des problèmes, contactez le support.</p>
|
||||
</div>
|
||||
<p v-if="interuptionMessage" class="error"><Icon icon="fa-solid fa-circle-xmark"/> {{ interuptionMessage }}</p>
|
||||
<p v-else>{{ defaultMessage }}</p>
|
||||
|
36
src/utils/Mouse.js
Normal file
36
src/utils/Mouse.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
export default function useMouse() {
|
||||
const position = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
isDown: false,
|
||||
});
|
||||
|
||||
function updateMousePosition(event) {
|
||||
position.value.x = event.clientX;
|
||||
position.value.y = event.clientY;
|
||||
}
|
||||
|
||||
function setMouseDown() {
|
||||
position.value.isDown = true;
|
||||
}
|
||||
|
||||
function setMouseUp() {
|
||||
position.value.isDown = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', updateMousePosition);
|
||||
window.addEventListener('mousedown', setMouseDown);
|
||||
window.addEventListener('mouseup', setMouseUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', updateMousePosition);
|
||||
window.removeEventListener('mousedown', setMouseDown);
|
||||
window.removeEventListener('mouseup', setMouseUp);
|
||||
});
|
||||
|
||||
return position;
|
||||
};
|
@@ -12,7 +12,7 @@
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
|
||||
import { useLoginStore } from '@/stores/loginStore';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {socket} from '@/socket.js';
|
||||
|
44
src/utils/TimeConverter.js
Normal file
44
src/utils/TimeConverter.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export function getReadableDuration(duration) {
|
||||
var max = ""
|
||||
duration *= 1000;
|
||||
|
||||
const maxhours = Math.floor(duration / 3600000);
|
||||
|
||||
var maxmin = Math.trunc(duration / 60000) - (Math.floor(duration / 60000 / 60) * 60);
|
||||
var maxsec = Math.floor(duration / 1000) - (Math.floor(duration / 1000 / 60) * 60);
|
||||
|
||||
|
||||
if (maxsec < 10) {
|
||||
maxsec = `0${maxsec}`;
|
||||
}
|
||||
|
||||
|
||||
if(maxhours != 0) {
|
||||
|
||||
if (maxmin < 10) {
|
||||
maxmin = `0${maxmin}`;
|
||||
}
|
||||
|
||||
|
||||
max = maxhours + "h" + maxmin + "m" + maxsec
|
||||
} else {
|
||||
max = maxmin + "m" + maxsec + "s"
|
||||
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
||||
|
||||
export function getSecondsDuration(duration) {
|
||||
// Duration is in format hh:mm:ss and can be just m:ss or mm:ss
|
||||
var durationArray = duration.split(":");
|
||||
var seconds = 0;
|
||||
if(durationArray.length == 3) {
|
||||
seconds = parseInt(durationArray[0]) * 3600 + parseInt(durationArray[1]) * 60 + parseInt(durationArray[2]);
|
||||
} else if(durationArray.length == 2) {
|
||||
seconds = parseInt(durationArray[0]) * 60 + parseInt(durationArray[1]);
|
||||
} else {
|
||||
seconds = parseInt(durationArray[0]);
|
||||
}
|
||||
return seconds;
|
||||
}
|
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<SocketEnvironment>
|
||||
<div class="container">
|
||||
<HeaderGuild/>
|
||||
<GuildHeader/>
|
||||
<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="IORequest('/MOD/USERS/BAN', null, '582966873201704960')">Bannir Raphixscrap_</Button>
|
||||
<Button @click="globalStore.toogleTheme()">Changer le thème</Button>
|
||||
<p>{{ guildId }}</p>
|
||||
</Box>
|
||||
<Search/>
|
||||
<Account/>
|
||||
</div>
|
||||
</SocketEnvironment>
|
||||
@@ -23,9 +25,10 @@ import { useGlobalStore } from '@/stores/globalStore';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { IOListener, IORequest } from '@/utils/IORequest';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import Account from '@/components/Features/Account.vue';
|
||||
import Account from '@/components/Layout/Account.vue';
|
||||
import events from '@/utils/Events.js';
|
||||
import HeaderGuild from '@/components/Features/HeaderGuild.vue';
|
||||
import GuildHeader from '@/components/Layout/GuildHeader.vue';
|
||||
import Search from '@/components/Layout/Search.vue';
|
||||
|
||||
const props = defineProps({
|
||||
guildId: {
|
||||
@@ -43,7 +46,6 @@ if (!guildId) {
|
||||
const globalStore = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const alreadyLoaded = ref(false);
|
||||
|
||||
watch(() => globalStore.isLoading, (value, oldValue) => {
|
||||
if(!value && oldValue != value) {
|
||||
@@ -52,7 +54,7 @@ watch(() => globalStore.isLoading, (value, oldValue) => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if(globalStore.lastGuild && !globalStore.isLoading) {
|
||||
if(!globalStore.isLoading) {
|
||||
loadInteface();
|
||||
}
|
||||
});
|
||||
@@ -62,16 +64,9 @@ events.on("UPDATE", () => {
|
||||
});
|
||||
|
||||
function loadInteface() {
|
||||
if(alreadyLoaded.value) return;
|
||||
console.log("Loading interface")
|
||||
console.log("Guild ID:", guildId);
|
||||
checkGuildAvailability();
|
||||
|
||||
IORequest("/USERS/LIST", (data) => {
|
||||
console.log("User list received:", data);
|
||||
})
|
||||
|
||||
alreadyLoaded.value = true;
|
||||
}
|
||||
|
||||
function checkGuildAvailability() {
|
||||
@@ -85,6 +80,7 @@ function checkGuildAvailability() {
|
||||
IORequest("/GUILD/JOIN", (response) => {
|
||||
if(response === true) {
|
||||
console.log("Successfully joined guild:", guildId);
|
||||
events.emit("GUILD_JOINED");
|
||||
} else {
|
||||
console.error("Failed to join guild:", response);
|
||||
globalStore.setLastGuild(null);
|
||||
@@ -102,11 +98,10 @@ function checkGuildAvailability() {
|
||||
<style scoped>
|
||||
|
||||
.container {
|
||||
padding: 20px;;
|
||||
padding: 15px;;;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -7,26 +7,27 @@
|
||||
<Error v-if="callbackError">{{ callbackError }}</Error>
|
||||
<Error v-if="error">{{ error }}</Error>
|
||||
<Error v-if="errorServer">{{ errorServer }}</Error>
|
||||
<Info v-if="message">{{ message }}</Info>
|
||||
<Success v-if="message">{{ message }}</Success>
|
||||
</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>
|
||||
<p class="secondtext"><router-link to="/privacy">Confidentialité</router-link></p>
|
||||
</div>
|
||||
<p class="second">Session : {{ sessionId }}</p>
|
||||
</DefaultSplash>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import DefaultSplash from '@/components/UI/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 { computed, ref, watch } from 'vue';
|
||||
import { socket } from '@/socket.js';
|
||||
import { useGlobalStore } from '@/stores/globalStore';
|
||||
import Success from '@/components/UI/Success.vue';
|
||||
|
||||
|
||||
const loginStore = useLoginStore();
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
|
||||
import Button from '@/components/UI/Button.vue'
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import { socket } from '@/socket';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import ReturnHomeButton from '@/components/Widget/ReturnHomeButton.vue';
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@ import Button from '@/components/UI/Button.vue';
|
||||
<template>
|
||||
<DefaultSplash>
|
||||
<ReturnHomeButton/>
|
||||
<h1>Privacy</h1>
|
||||
<h1>Confidentialité</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>
|
||||
@@ -116,4 +116,9 @@ import Button from '@/components/UI/Button.vue';
|
||||
color: var(--text-secondary);
|
||||
text-align: justify;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import Splash from '@/components/Layout/Splash.vue';
|
||||
import Splash from '@/components/UI/Splash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import SocketEnvironment from "../utils/SocketEnvironment.vue"
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -8,9 +8,9 @@ import { useUserStore } from '@/stores/userStore';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
import ReturnHomeButton from '@/components/Widget/ReturnHomeButton.vue';
|
||||
import { ref } from 'vue';
|
||||
import ServerListItem from '@/components/Widget/ServerListItem.vue';
|
||||
import ServerListItem from '@/components/Widget/Server/ServerListItem.vue';
|
||||
import Info from '@/components/UI/Info.vue';
|
||||
import Account from '@/components/Features/Account.vue';
|
||||
import Account from '@/components/Layout/Account.vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import ReturnHomeButton from '@/components/Widget/ReturnHomeButton.vue';
|
||||
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
|
||||
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
|
||||
import Box from '@/components/UI/Box.vue';
|
||||
import Button from '@/components/UI/Button.vue';
|
||||
</script>
|
||||
@@ -11,12 +11,16 @@ import Button from '@/components/UI/Button.vue';
|
||||
<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>
|
||||
<p>En accédant à cette plateforme et en vous connectant via Discord <Icon icon="fa-solid fa-copyright"/>, vous acceptez automatiquement les présentes conditions d’utilisation ainsi que la <strong>politique de confidentialité</strong>.
|
||||
Les données fournies par Discord sont utilisées uniquement pour assurer le bon fonctionnement du service. Vous pouvez à tout moment demander la suppression de vos données en contactant <strong>Raphix</strong> (<code>raphixscrap</code>) ou bien en allant dans les paramètres de votre compte en utilisant le petit engrenage à coté de votre photo de profil et en cliquant sur le boutton "Supprimer mon compte".</p>
|
||||
<p>En vous connectant, vous acceptez que votre identifiant Discord (ID et @) soit gardé à jamais pour des raisons de sécurité (bannissement). Cependant toutes les playlists crées, votre historique de musique, les informations seront supprimés.</p>
|
||||
<p>Tout acte de sabotage, de tentative de perturbation ou de dégradation intentionnelle du bot entraînera un bannissement immédiat et définitif du site.</p> <p><strong>Subsonics est un outil privé</strong>, non destiné à une utilisation publique ou commerciale.</p>
|
||||
<p>Les musiques disponibles via le service sont diffusées à partir des plateformes suivantes : <strong>YouTube</strong>, <strong>SoundCloud</strong> et <strong>Spotify</strong>.
|
||||
Subsonics ne stocke aucun fichier audio. Vous bénéficiez uniquement d’un droit d’accès au service et à l’écoute du contenu, mais <strong>Raphix</strong> ne fournit ni la provenance, ni les autorisations légales liées à l'exploitation des œuvres. Il vous revient d’obtenir les autorisations nécessaires auprès des ayants droit si nécessaire.</p>
|
||||
<p>En utilisant Subsonics, vous vous engagez à ne pas diffuser de contenu :</p>
|
||||
<ul> <li>illégal, violent, haineux, discriminatoire ou à caractère sexuel explicite,</li>
|
||||
<li>à caractère politique, religieux, publicitaire, incitant à la haine ou pornographique.</li> </ul>
|
||||
<p>Toute infraction à ces règles pourra entraîner des sanctions immédiates, y compris l’exclusion définitive du service.</p>
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
@@ -40,4 +44,10 @@ import Button from '@/components/UI/Button.vue';
|
||||
color: var(--text-secondary);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user