Version 1.2.0 - Ajout des suggestions et de paramètres
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 35s

This commit is contained in:
2025-09-07 18:18:54 +02:00
parent b2a95c0241
commit ea9cf2ce42
26 changed files with 315 additions and 38 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "chopin-frontend",
"version": "1.1.0",
"version": "1.2.0",
"private": true,
"scripts": {
"dev": "vite --host --port 8080",

View File

@@ -31,8 +31,6 @@ const router = useRouter();
const userSettings = ref(null);
//FIXME: Set to dispatcher
</script>
<style scoped>
.box {

View File

@@ -25,8 +25,8 @@
</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>
<Button icon="fa-solid fa-gear" v-if="gestion" @click="settings.open()">Gestion</Button>
<Button icon="fa-solid fa-arrow-right" :style="{width: gestion ? '' : '100%'}" @click="router.push(`/servers`)">Changer de serveur</Button>
</div>
</div>
</div>

View File

@@ -112,9 +112,6 @@ const playerOpen = ref(false);
const durationBar = ref(null);
const buffering = ref(false);
//TODO: Rework Animation Both Side
//FIXME: Animation weird 550px
const playerMobile = ref(null);
const playerMobileToggle = ref(null);
const playerHeight = ref(0);

View File

@@ -12,7 +12,7 @@
</p>
</div>
</div>
<Button class="add-playlist" @click="openModal()"><Icon icon="fa-add"/> Ajouter une playlist</Button>
<Button icon="fa-add" class="add-playlist" @click="openModal()">Ajouter une playlist</Button>
</div>
<Modal icon="fa-list" title="Ajouter une Playlist" ref="modal">
<div class="p-modal-content">
@@ -23,7 +23,7 @@
<label>Importer depuis Youtube</label>
</div>
<Button :disabled="(!isYoutube && newPlaylistTitle.trim() === '') || (isYoutube && urlLink?.trim() === '')" @click="addPlaylist">Ajouter</Button>
<Button icon="fa-add" :disabled="(!isYoutube && newPlaylistTitle.trim() === '') || (isYoutube && urlLink?.trim() === '')" @click="addPlaylist">Ajouter</Button>
<p class="text-loading" v-if="isLoading"><Icon icon="fa-spinner" spin-pulse /> Création en cours...</p>
<p class="text-loading" v-if="error"><Error> {{ error }}</Error></p>
</div>

View File

@@ -1,7 +1,11 @@
<template>
<div class="search">
<Icon color="#FFFFFF" icon="fa-solid fa-magnifying-glass" style="width: 20px;" />
<input name="search" ref="searchBar" type="text" placeholder="Insérer votre recherche ici" v-model="searchQuery" />
<div class="search-bar">
<input autocomplete="off" name="search" ref="searchBar" type="text" placeholder="Insérer votre recherche ici" v-model="searchQuery" />
<Icon color="#FFFFFF" v-if="searchQuery.trim() !== ''" icon="fa-solid fa-xmark" class="clear-icon" @click="searchQuery = ''" />
<SearchSuggestions v-if="searchQuery.trim() !== '' && suggestClose" :query="searchQuery" />
</div>
<div class="search-actions">
<IconAction
icon="fa-solid fa-cloud-arrow-up"
@@ -25,17 +29,24 @@ import { onBeforeUnmount, onMounted, ref } from 'vue';
import IconAction from '../UI/IconAction.vue';
import { IORequest } from '@/utils/IORequest';
import Events from '@/utils/Events';
import SearchSuggestions from '@/components/Widget/View/Search/SearchSuggestions.vue';
const searchBar = ref(null);
const searchQuery = ref('');
const suggestClose = ref(true);
onMounted(() => {
searchBar.value.addEventListener('change', find);
searchBar.value.addEventListener('change', () => {
find();
suggestClose.value = true;
});
searchBar.value.addEventListener('keydown', (event) => {
suggestClose.value = true;
});
});
//TODO: Faire un systême de suggestions.
function find() {
function find(queryByResult) {
// If on mobile close the keyboard
if (window.innerWidth < 768) {
searchBar.value.blur();
@@ -43,9 +54,15 @@ function find() {
if (searchQuery.value.trim() === '') {
return;
}
if(queryByResult == "result") {
Events.emit("SEARCH_RESULT_SENDED");
}
Events.emit("SEARCH_STARTED");
IORequest("/SEARCH", (data) => {
Events.emit("SEARCH_RESULT", {data: data, query: searchQuery.value});
if(queryByResult == "result") {
Events.emit("SEARCH_RESULT_SENDED");
}
}, searchQuery.value);
}
@@ -53,6 +70,13 @@ Events.on("VIEW_CLOSED", () => {
searchQuery.value = '';
});
Events.on("SEARCH_RESULT_SELECTED", (result) => {
searchQuery.value = result;
suggestClose.value = false;
Events.emit("SEARCH_RESULT_SENDED");
find("result");
});
</script>
@@ -68,6 +92,19 @@ Events.on("VIEW_CLOSED", () => {
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.50);
}
.clear-icon {
cursor: pointer;
position: absolute;
right: 10px;
}
.search-bar {
position: relative;
flex: 1;
display: flex;
align-items: center;
}
.search input {
border: none;
outline: none;

View File

@@ -26,8 +26,6 @@ const disableClass = computed(() => {
return props.disabled ? props.colorLower ? 'disabled color-lower' : 'disabled' : '';
});
//TODO: Refactor every button component to use the icon prop
</script>
<style scoped>
button {

View File

@@ -65,6 +65,7 @@ function open() {
if (modal.value) {
modal.value.style.display = 'flex';
}
Events.emit('modal:open');
}
defineExpose({

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup>
import IconAction from './IconAction.vue';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { useSlots } from 'vue';

View File

@@ -2,7 +2,11 @@
<div class="subsonics-logo">
<LogoDark class="img" v-if="globalStore.theme == 'light'"/>
<LogoLight class="img" v-else/>
<h1>Subsonics</h1>
<div v-if="dev" class="text-p">
<h1>Subsonics</h1>
<p>Developement</p>
</div>
<h1 v-if="!dev">Subsonics</h1>
</div>
</template>
<script setup>
@@ -10,6 +14,8 @@ import LogoDark from '@/assets/LogoDark.vue';
import LogoLight from '@/assets/LogoLight.vue';
import { useGlobalStore } from '@/stores/globalStore';
const dev = import.meta.env.DEV;
const globalStore = useGlobalStore();
</script>
<style scoped>
@@ -27,6 +33,25 @@ const globalStore = useGlobalStore();
justify-content: center;
}
.text-p {
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
}
.text-p h1 {
margin: 0 !important;
}
.text-p p {
margin: 0 !important;
font-family: 'Gunship', sans-serif;
position: absolute;
bottom: -10px;
font-size: 0.8em;
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.img {

View File

@@ -3,9 +3,11 @@
<ModalTree title="Utilisateurs" icon="fa-solid fa-users">
<GuildUsers :server="server"/>
</ModalTree>
<ModalTree v-if="userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.identity.id == server.owner" title="Sécurité" icon="fa-solid fa-shield-halved">
<GuildSecurity :server="server"/>
</ModalTree>
<ModalTree title="Statistiques" icon="fa-solid fa-chart-simple">
<GuildStats :server="server"/>
</ModalTree>
</Modal>
</template>
@@ -15,13 +17,14 @@ import ModalTree from '@/components/UI/ModalTree.vue';
import { ref } from 'vue';
import GuildUsers from './Settings/GuildUsers.vue';
import GuildStats from './Settings/GuildStats.vue';
import GuildSecurity from './Settings/GuildSecurity.vue';
import Events from '@/utils/Events';
import { useUserStore } from '@/stores/userStore';
const userStore = useUserStore();
const modal = ref(null);
//TODO: Ajouter la sécurité des roles pour empêcher l'utilisation publique du Bot
//TODO: Ajout de Log pour serveur
//TODO: Paramétérer une liste des channels autorisé !
const props = defineProps({
server: {

View File

@@ -0,0 +1,93 @@
<template>
<div class="setting">
<p class="text-secondary"><Icon icon="fa-solid fa-user-shield" /> Autoriser les utilisateurs avec un rôle spécifique</p>
<Selector v-model="roleSelected" ref="roleSelector" v-if="roles.length > 0 && modalOpen">
<Tag v-for="role in roles" :key="role.id" :value="role.id" :color="decimalToHex(role.color)">{{role.name.replace("@everyone", "Tout le monde (@everyone)")}}</Tag>
</Selector>
<Button @click="updateRole" icon="fa-solid fa-rotate">Mettre à jour</Button>
<Success v-if="roleUpdated">Rôle mis à jour avec succès !</Success>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import Selector from '@/components/UI/Selector.vue';
import { IORequest } from '@/utils/IORequest';
import Tag from '@/components/UI/Tag.vue';
import Events from '@/utils/Events';
import Button from '@/components/UI/Button.vue';
import Success from '@/components/UI/Success.vue';
const roles = ref([])
const roleSelector = ref(null);
const roleUpdated = ref(false);
const roleSelected = ref(null);
const modalOpen = ref(false);
const props = defineProps({
server: {
type: Object,
required: true
}
});
//TODO: FINISH IMPLEMENTATION
onMounted(() => {
actualizeRoles();
})
Events.on("modal:open", () => {
actualizeRoles();
modalOpen.value = true;
roleUpdated.value = false;
})
Events.on("modal:close", () => {
modalOpen.value = false;
roleSelected.value = null;
roleUpdated.value = false;
})
function actualizeRoles() {
IORequest("/OWNER/ROLES/GET", (data) => {
roles.value = data;
roleSelected.value = null;
});
}
function updateRole() {
console.log(roleSelector.value?.firstSlot());
IORequest("/OWNER/ROLES/SET", (data) => {
actualizeRoles();
roleUpdated.value = true;
setTimeout(() => {
roleUpdated.value = false;
}, 3000);
}, roles.value.find(r => r.id === roleSelected.value) || null);
}
function decimalToHex(decimal) {
if(decimal == 0) return "var(--quaternary)"; // Default color
let hex = Number(decimal).toString(16);
hex = "000000".substring(0, 6 - hex.length) + hex;
return `#${hex}`;
}
</script>
<style scoped>
.text-secondary {
color: var(--text-secondary);
font-size: 0.875rem;
margin-bottom: 0.5rem;
text-decoration: underline;
margin: 0;
}
.setting {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
</style>

View File

@@ -41,7 +41,7 @@
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: nextList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: nextList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()" icon="fa-solid fa-save">Sauvegarder</Button>
</Modal>
</section>
</template>

View File

@@ -30,7 +30,7 @@
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: previousList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: previousList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()" icon="fa-solid fa-save">Sauvegarder</Button>
</Modal>
</section>
</template>

View File

@@ -5,7 +5,8 @@
<ServerOnlinePicture class="sop" :key="server.id" v-if="server.members.length > 0" :members="server.members"/>
<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>
<Error v-else-if="server.restricted">Restreint</Error>
<Button color-lower :disabled="userStore.userInfo.labels.includes('BAN_' + server.id) || server.restricted" class="btn" @click="access()" icon="fa-solid fa-right-to-bracket">Accéder</Button>
</section>
</div>
</Box>

View File

@@ -54,7 +54,7 @@ watch(showForm, (newValue) => {
<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>
<Button v-if="!showForm" @click="showForm = !showForm" class="margin" 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>

View File

@@ -1,7 +1,7 @@
<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>
<Button @click="openModal()" 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/>

View File

@@ -10,13 +10,13 @@
<Modal icon="fa-solid fa-video" title="Actions" ref="modal">
<Video :video="video"/>
<Button v-if="globalStore.currentChannel" @click="playSong(false)"><AddList /> Ajouter à la liste de lecture</Button>
<Button v-if="globalStore.currentChannel" @click="playSong(true)"><Icon icon="fa-solid fa-play"/> Lire maintenant</Button>
<Button icon="fa-solid fa-play" v-if="globalStore.currentChannel" @click="playSong(true)"> Lire maintenant</Button>
<div v-if="!globalStore.currentChannel" >
<p class="text-secondary">Connectez vous à un salon audio sur le serveur {{ globalStore.actualServer ? globalStore.actualServer.name : '' }}, pour lancer un titre</p>
<ActualChannel/>
</div>
<Button v-if="!props.delete && video.type != 'attachment'" @click="saveModal.open()"><Icon icon="fa-solid fa-save" /> Enregistrer dans une playlist</Button>
<Button v-if="props.delete" @click="Events.emit('video:delete', { video: props.video })"><Icon icon="fa-solid fa-trash" /> Supprimer</Button>
<Button v-if="!props.delete && video.type != 'attachment'" @click="saveModal.open()" icon="fa-solid fa-save"> Enregistrer dans une playlist</Button>
<Button v-if="props.delete" @click="Events.emit('video:delete', { video: props.video })" icon="fa-solid fa-trash"> Supprimer</Button>
</Modal>
<Modal ref="saveModal" icon="fa-save" title="Sauvegarder dans une playlist">
<Video class="save-video" :video="video"/>
@@ -24,7 +24,7 @@
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: props.video, playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: props.video, playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()" icon="fa-solid fa-save" > Sauvegarder</Button>
</Modal>
</template>
<script setup>

View File

@@ -4,7 +4,7 @@
<img v-if="results.thumbnail" class="search-playlist-thumbnail" :src="results.thumbnail" alt="Playlist Thumbnail" />
<div v-else class="search-playlist-thumbnail"><div class="defaultIcon"><Icon icon="fa-music" /></div></div>
<div class="search-playlist-info">
<p class="search-playlist-title">{{ results.title }} <Icon @click="openPlaylistPage()" class="link-icon" icon="fa-solid fa-link" /></p>
<p class="search-playlist-title">{{ results.title }} <Icon v-if="results.url" @click="openPlaylistPage()" class="link-icon" icon="fa-solid fa-link" /></p>
<p class="search-playlist-stats"><Tag color="var(--text-warning)">{{ results.songs.length }} titres</Tag><Tag v-if="results.views" color="var(--text-success)">{{ results.views }} vues</Tag ><Tag v-if="results.songs.length > 0">Durée : {{ results.readduration }}</Tag></p>
<div @click="openAuthorPage()" class="search-playlist-author-info">
<img v-if="results.authorAvatar" :src="results.authorAvatar" alt="Author Thumbnail" class="search-playlist-author" />

View File

@@ -0,0 +1,116 @@
<template>
<div ref="suggestions" class="search-suggestions">
<ul>
<li class="search-suggestion" v-for="result in results" :key="result.id" @click="selectResult(result[0])">
{{ result[0] }}
<Icon class="search-icon" icon="fa-solid fa-arrow-up-right-from-square" style="float: right;"/>
</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import Events from '@/utils/Events';
const props = defineProps({
query: String
});
const results = ref([]);
const suggestions = ref(null);
onMounted(() => {
window.addEventListener('click', (event) => {
if (suggestions.value && !suggestions.value.contains(event.target)) {
results.value = [];
}
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
results.value = [];
}
if (event.key === 'Enter') {
results.value = [];
}
});
})
watch(() => props.query, (newQuery) => {
if (newQuery) {
fetchSuggest(newQuery)
.then(data => {
results.value = data[1];
});
} else {
results.value = [];
}
});
function fetchSuggest(query) {
return new Promise((resolve, reject) => {
const callbackName = "jsonp_callback_" + Math.random().toString(36).substr(2, 5);
window[callbackName] = function(data) {
resolve(data);
delete window[callbackName];
script.remove();
};
const script = document.createElement("script");
script.src = `https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=${encodeURIComponent(query)}&callback=${callbackName}`;
script.onerror = reject;
document.body.appendChild(script);
});
}
function selectResult(result) {
Events.emit("SEARCH_RESULT_SELECTED", result);
}
Events.on("SEARCH_RESULT_SENDED", () => {
results.value = [];
})
</script>
<style scoped>
.search-suggestions {
position: absolute;
background: var(--secondary);
opacity: 0.95;
z-index: 1000;
width: 100%;
top: 40px;
max-height: 30vh;
overflow-y: auto;
border-radius: 10px;
}
.search-suggestions ul {
list-style: none;
padding: 0;
margin: 0;
}
.search-suggestions li {
padding: 10px;
cursor: pointer;
}
.search-icon {
display: none;
opacity: 0.5;
}
.search-suggestion:hover .search-icon {
display: block !important;
}
.search-suggestions li:hover {
background: var(--tertiary);
}
</style>

View File

@@ -4,7 +4,7 @@
<h2><Icon icon="fa-folder"/> Mes fichiers</h2>
<Box box-class="area-container" padding="close" level="second">
<div class="upload-area">
<Button :color-lower="status === 'download'" :disabled="status === 'download'" class="upload-button" @click="uploadFile()"><Icon icon="fa-upload"/> Ajouter des fichiers</Button>
<Button :color-lower="status === 'download'" :disabled="status === 'download'" class="upload-button" @click="uploadFile()" icon="fa-upload"> Ajouter des fichiers</Button>
<p v-if="status === 'download'" class="upload-file-name" ref="uploadStatus"><Icon icon='fa-spinner' spin-pulse/> Téléchargement en cours...</p>
<p v-else-if="status === 'error'" class="upload-status" ref="uploadStatus"><Error>Erreur lors du téléchargement</Error></p>
<p v-else-if="status === 'toohigh'" class="upload-status" ref="uploadStatus"><Error>Le fichier est trop volumineux</Error></p>

View File

@@ -1,10 +1,11 @@
<script setup>
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import MusicAnimation from '@/components/UI/MusicAnimation.vue';
import { version } from '@/../package.json';
const defaultMessage = "On s'accorde et on prépare le concert !";
const connectMsg = "Erreur de connexion au serveur : xhr poll error"
const props = defineProps({
interuptionMessage: {
type: String,
@@ -26,6 +27,7 @@ const props = defineProps({
<p v-if="interuptionMessage" class="error"><Icon icon="fa-solid fa-circle-xmark"/> {{ interuptionMessage }}</p>
<p v-else>{{ defaultMessage }}</p>
<MusicAnimation />
<p class="version">Version : {{ version }} - Chopin</p>
</DefaultSplash>
</template>
<style scoped>
@@ -63,6 +65,12 @@ const props = defineProps({
color: var(--text-secondary);
}
.version {
font-size: 0.6em;
margin-bottom: 0;
color: var(--text-secondary);
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
h1 {

View File

@@ -10,7 +10,7 @@
</div>
<br>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
<Button icon="fa-solid fa-house"> Revenir au concert</Button>
</router-link>
</DefaultSplash>

View File

@@ -103,7 +103,7 @@ onMounted(() => {
</Box>
<br/>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
<Button icon="fa-solid fa-house"> Revenir au concert</Button>
</router-link>
<!-- Add more content here as needed -->
</DefaultSplash>

View File

@@ -61,7 +61,7 @@ function inviteSubsonics() {
</div>
</div>
<Button :disabled="!hasLink" @click="inviteSubsonics()"><Icon icon="fa-solid fa-user-plus"/>Inviter Subsonics</Button>
<Button :disabled="!hasLink" @click="inviteSubsonics()" icon="fa-solid fa-user-plus">Inviter Subsonics</Button>
</div>
</Box>
<Account class="full"/>

View File

@@ -31,7 +31,7 @@ onMounted(() => {
</Box>
<br>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
<Button icon="fa-solid fa-house">Revenir au concert</Button>
</router-link>
<!-- Add more content here as needed -->
</DefaultSplash>