diff options
Diffstat (limited to 'ping/frontend/src/lib/components')
14 files changed, 1072 insertions, 0 deletions
diff --git a/ping/frontend/src/lib/components/Avatar.svelte b/ping/frontend/src/lib/components/Avatar.svelte new file mode 100644 index 0000000..c04b563 --- /dev/null +++ b/ping/frontend/src/lib/components/Avatar.svelte @@ -0,0 +1,48 @@ +<script lang="ts"> + let showTooltip = $state(false); + + interface Props { + username?: string; + url?: string; + onclick?: () => void; + } + + let { username = 'lolo', url = '/img/default-avatar.png', onclick = () => {} }: Props = $props(); +</script> + +<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> +<div style="position: relative; display: inline-block;"> + <!-- svelte-ignore a11y_click_events_have_key_events --> + <img + src={url ?? '/img/default-avatar.png'} + onmouseenter={() => (showTooltip = true)} + onmouseleave={() => (showTooltip = false)} + {onclick} + alt="User Avatar" + /> + {#if showTooltip} + <div class="tooltip">{username}</div> + {/if} +</div> + +<style> + img { + width: 48px; + height: 48px; + border-radius: 50%; + } + .tooltip { + position: absolute; + top: 110%; + left: 50%; + transform: translateX(-50%); + background: #333; + color: #fff; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + font-size: 0.9em; + z-index: 1; + pointer-events: none; + } +</style> diff --git a/ping/frontend/src/lib/components/Button.svelte b/ping/frontend/src/lib/components/Button.svelte new file mode 100644 index 0000000..54b26ab --- /dev/null +++ b/ping/frontend/src/lib/components/Button.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + // Accept props and ensure onclick matches the expected event handler signature + const { + href = undefined as string | undefined, + onclick = undefined as + | ((event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) => void) + | undefined, + disabled = false, + children + } = $props(); +</script> + +{#if href} + <a class="btn" {href}>{@render children?.()}</a> +{:else} + <button class="btn" {onclick} {disabled}>{@render children?.()}</button> +{/if} diff --git a/ping/frontend/src/lib/components/NavBar.svelte b/ping/frontend/src/lib/components/NavBar.svelte new file mode 100644 index 0000000..1a76876 --- /dev/null +++ b/ping/frontend/src/lib/components/NavBar.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + const { pageTitle, rightComponent } = $props(); +</script> + +<nav> + <a id="logo" href="/"> + <img src="/img/logo.svg" alt="" style="height:50px" /> + </a> + <div id="middletitle">{pageTitle}</div> + <div id="right">{@render rightComponent()}</div> +</nav> + +<style> + nav { + position: fixed; + width: 100%; + background-color: var(--bg-primary); + color: white; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + height: 64px; + z-index: 2; + } + + #logo { + transition-duration: 0.2s; + } + + #logo:hover { + transform: scale(1.025); + filter: saturate(2); + } + + #middletitle { + font-weight: bold; + font-size: 24px; + } + + #right { + display: flex; + } +</style> diff --git a/ping/frontend/src/lib/components/NumberStatList.svelte b/ping/frontend/src/lib/components/NumberStatList.svelte new file mode 100644 index 0000000..b6444e8 --- /dev/null +++ b/ping/frontend/src/lib/components/NumberStatList.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + export interface INumberStatList { + name: string; + value: string; + icon: string; + color: string; + } + + const { statsList = [] }: { statsList?: INumberStatList[] } = $props(); +</script> + +<section class="numberStatList"> + {#each statsList as stat} + <div class="card"> + <img src={stat.icon} alt="" class="stat-icon" /> + <span class="stat-name">{stat.name}</span> + <span class="stat-value" style:color={stat.color}>{stat.value}</span> + </div> + {/each} +</section> + +<style> + .numberStatList { + color: white; + display: flex; + width: 100%; + align-items: stretch; + justify-content: stretch; + } + + .numberStatList :global(.card) { + flex: 1; + display: grid; + grid-template-areas: 'icon name' 'icon amount'; + align-items: center; + justify-content: space-around; + } + + .stat-icon { + width: 24px; + height: 24px; + grid-area: icon; + } + + .stat-name { + grid-area: name; + font-weight: bold; + } + + .stat-value { + grid-area: amount; + font-size: 1.2em; + font-weight: bold; + } +</style> diff --git a/ping/frontend/src/lib/components/SideBar.svelte b/ping/frontend/src/lib/components/SideBar.svelte new file mode 100644 index 0000000..9ea8a04 --- /dev/null +++ b/ping/frontend/src/lib/components/SideBar.svelte @@ -0,0 +1,61 @@ +<script lang="ts"> + import { pages } from '$lib/pages'; + + const { + selectedIndex = $bindable(0) + }: { + selectedIndex: number; + } = $props(); + + const items = pages; +</script> + +<section id="sidebar"> + {#each items as item, index} + <!-- svelte-ignore a11y_consider_explicit_label --> + <a href={item.href} class:selected={index == selectedIndex} class="flex items-center gap-2"> + <img + src={item.icon} + alt=" " + class="w-6" + style="filter: {index == selectedIndex + ? 'invert(68%) sepia(97%) saturate(749%) hue-rotate(97deg) brightness(154%) contrast(101%)' + : 'none'};" + /> + <span>{item.name}</span> + </a> + {/each} +</section> + +<style> + #sidebar { + position: fixed; + background-color: var(--bg-primary); + height: 100vh; + width: 200px; + padding-top: 72px; + color: white; + display: flex; + flex-direction: column; + gap: 8px; + } + + #sidebar > a { + padding: 8px; + padding-right: 0; + transition-duration: 0.2s; + border-color: var(--text-lime); + } + + #sidebar > a:is(:hover, :focus) { + background-color: var(--bg-secondary); + } + + .selected { + color: var(--text-lime); + background-color: var(--bg-secondary); + font-weight: bold; + border-right: 6px var(--text-lime) solid; + width: 100%; + } +</style> diff --git a/ping/frontend/src/lib/components/SteppedLineChart.svelte b/ping/frontend/src/lib/components/SteppedLineChart.svelte new file mode 100644 index 0000000..d393b44 --- /dev/null +++ b/ping/frontend/src/lib/components/SteppedLineChart.svelte @@ -0,0 +1,76 @@ +<script lang="ts"> + import { + Chart, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + LineController + } from 'chart.js'; + import { onMount } from 'svelte'; + + Chart.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + LineController + ); + + interface IProps { + color?: string; + data?: any[]; + title?: string; + legend?: string; + } + + const { color = 'green', data = [], title = '', legend = '' }: IProps = $props(); + + function updateGraph() { + const ctx = document.getElementById(id) as HTMLCanvasElement; + const chart = new Chart(ctx, { + type: 'line', + data: { + labels: data.map((d) => d.label), + datasets: [ + { + label: legend, + data: data.map((d) => d.value), + borderColor: color, + backgroundColor: color, + fill: false, + tension: 0.1 + } + ] + }, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: title + }, + legend: { + display: true + } + } + } + }); + } + + const id = 'graph-' + crypto.randomUUID(); + + onMount(() => { + updateGraph(); + }); +</script> + +<div> + <canvas {id}></canvas> +</div> diff --git a/ping/frontend/src/lib/components/ToastList.svelte b/ping/frontend/src/lib/components/ToastList.svelte new file mode 100644 index 0000000..5351ef7 --- /dev/null +++ b/ping/frontend/src/lib/components/ToastList.svelte @@ -0,0 +1,67 @@ +<script lang="ts"> + import { toastList } from '$lib/stores/toast'; +</script> + +<div class="toast"> + {#each $toastList as toast} + <div class="toast-item" style="border-top: 4px {toast.color} solid"> + <h3 style="color: {toast.color}">{toast.title}</h3> + <p>{toast.message}</p> + </div> + {/each} +</div> + +<style> + .toast { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 10px; + } + + .toast-item { + background-color: var(--bg-secondary); + padding: 10px; + color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: + transform 0.3s, + opacity 0.3s; + transform: translateY(30px); + opacity: 0; + animation-delay: 0s, 4.7s; + animation-name: toast-in, toast-out; + animation-duration: 0.3s, 0.3s; + animation-fill-mode: forwards, forwards; + } + + @keyframes toast-in { + from { + transform: translateY(30px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes toast-out { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(30px); + opacity: 0; + } + } + + .toast-item h3 { + margin: 0; + font-weight: bold; + } +</style> diff --git a/ping/frontend/src/lib/components/UserItem.svelte b/ping/frontend/src/lib/components/UserItem.svelte new file mode 100644 index 0000000..44add60 --- /dev/null +++ b/ping/frontend/src/lib/components/UserItem.svelte @@ -0,0 +1,42 @@ +<script lang="ts"> + import Avatar from './Avatar.svelte'; + import type { IUser } from '$lib/stores/auth'; + + const { user, onEdit, onDelete }: { user: IUser; onEdit: any; onDelete: any } = $props(); +</script> + +<div class="useritem"> + <div class="useritem-avatar"> + <Avatar username={user.displayName} url={user.avatar || '/img/default-avatar.png'} /> + <button class="btn" onclick={onEdit}> Modifier </button> + <button class="btn" onclick={onDelete}> Supprimer </button> + </div> + <div class="useritem-details"> + <p><b>Nom</b>: {user.displayName}</p> + <p><b>Login</b>: {user.login}</p> + <p><b>Role</b>: {user.isAdmin ? 'ADMIN' : 'USER'}</p> + <p><b>Id</b>: {user.id}</p> + </div> +</div> + +<style> + .useritem { + display: flex; + background-color: var(--bg-secondary); + margin: 16px 0; + padding: 8px; + border-radius: 8px; + gap: 8px; + } + + .useritem-avatar, + .useritem-details { + display: flex; + flex-direction: column; + gap: 4px; + } + + .useritem-avatar { + align-items: center; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte new file mode 100644 index 0000000..3844abc --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte @@ -0,0 +1,10 @@ +<h2>Analyse de risque</h2> +<i>tkt t safe c hardcodé chef</i> + +<style> + h2 { + font-weight: bold; + color: var(--text-lime); + font-size: 24px; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/StockGraph.svelte b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte new file mode 100644 index 0000000..beefed9 --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte @@ -0,0 +1,136 @@ +<script lang="ts"> + import Chart from 'chart.js/auto'; + import { onMount } from 'svelte'; + import StockSelector from '../input/StockSelector.svelte'; + + async function fetchChartData(stock: string, startDate: string, endDate: string) { + const res = await fetch( + `/stocksapi/chart?query=${stock}&startDate=${startDate}&endDate=${endDate}&interval=${range}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + } + } + ); + return await res.json(); + } + + function updateGraph() { + fetchChartData(selectedStock, startDate, endDate).then((data) => { + if (chart) { + chart.destroy(); + chart = null; + } + + const { quotes, meta } = data; + + validRanges = meta.validRanges; + + const labels = quotes.map((item: any) => item.date.split('T')[0]); + const datasets = ['low', 'high', 'open', 'close'].map((key) => ({ + label: key.charAt(0).toUpperCase() + key.slice(1), + data: quotes.map((item: any) => item[key]), + borderColor: + key === 'low' + ? 'rgb(255, 99, 132)' + : key === 'high' + ? 'rgb(54, 162, 235)' + : key === 'open' + ? 'rgb(255, 205, 86)' + : 'rgb(75, 192, 192)', + fill: false + })); + + // @ts-ignore + chart = new Chart(document.getElementById('stockgraph'), { + type: 'line', + data: { + labels, + datasets + }, + options: { + responsive: true, + maintainAspectRatio: true, + plugins: { + title: { + display: true, + text: meta.longName + } + } + } + }); + }); + } + + let today = $state(new Date().toISOString().split('T')[0]); + + let selectedStock = $state('2223.SR'); + let startDate = $state(''); + let endDate = $state(''); + let chart = $state<Chart | null>(null); + let range = $state('1d'); + let validRanges = $state(['1d']); + + onMount(() => { + endDate = new Date().toISOString().split('T')[0]; + startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + updateGraph(); + }); + + $effect(() => { + if (selectedStock && startDate && endDate && range) { + updateGraph(); + } + }); + + $effect(() => { + if (startDate && endDate && startDate > endDate) { + let temp = startDate; + startDate = endDate; + endDate = temp; + } + }); +</script> + +<div> + <div class="header"> + <h2>Vue d'ensemble : {selectedStock}</h2> + <div class="controls"> + <input type="date" name="startDate" id="startDate" bind:value={startDate} max={today} /> + <input type="date" name="endDate" id="endDate" bind:value={endDate} max={today} /> + <select name="validRanges" id="validRanges" bind:value={range}> + {#each validRanges as r} + <option value={r}> + {r} + </option> + {/each} + </select> + + <StockSelector bind:selectedStock /> + </div> + </div> + <canvas id="stockgraph"></canvas> +</div> + +<style> + .header { + display: flex; + align-items: center; + justify-content: space-between; + max-height: 512px !important; + } + + h2 { + font-weight: bold; + color: var(--text-lime); + font-size: 24px; + } + + #stockgraph { + width: 100% !important; + max-height: 512px !important; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte new file mode 100644 index 0000000..3d947ad --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte @@ -0,0 +1,85 @@ +<script lang="ts"> + import { Chart } from 'chart.js'; + import { onMount } from 'svelte'; + + function updateGraph() { + getTrendingSymbols().then((trendingSymbols) => { + const labels = trendingSymbols.map((d) => d.longName); + + const datasets = [ + { + label: '50 day performance', + data: trendingSymbols.map((ts) => ts.fiftyDayAverageChange), + backgroundColor: 'green' + }, + { + label: '52 week performance', + data: trendingSymbols.map((ts) => ts.fiftyTwoWeekLowChange), + backgroundColor: 'orange' + } + ]; + + const data = { labels, datasets }; + + // @ts-ignore + new Chart(document.getElementById('graph'), { + type: 'bar', + data: data, + options: { + indexAxis: 'y', + elements: { + bar: { + borderWidth: 2 + } + }, + responsive: true, + plugins: { + legend: { + position: 'bottom' + } + } + } + }); + }); + } + + async function getTrendingSymbols() { + const trendingRes = await fetch('/stocksapi/trendingSymbols'); + if (!trendingRes.ok) { + throw new Error('Failed to fetch trending symbols'); + } + const trendingJson = await trendingRes.json(); + + const { quotes } = trendingJson; + + const trendingSymbols = await Promise.all( + quotes.map(async (quote: any) => { + const quoteRes = await fetch(`/stocksapi/quote?query=${quote.symbol}`); + const quoteJson = await quoteRes.json(); + return quoteJson; + }) + ); + + return trendingSymbols; + } + + onMount(() => { + updateGraph(); + }); +</script> + +<h2>Tendances</h2> +<canvas id="graph"></canvas> + +<style> + h2 { + font-weight: bold; + color: var(--text-lime); + font-size: 24px; + } + + #graph { + max-height: 40vh; + max-width: 40vw; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte new file mode 100644 index 0000000..b9f0224 --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte @@ -0,0 +1,165 @@ +<script lang="ts"> + import { authFetch } from '$lib/stores/auth'; + import { onMount } from 'svelte'; + + interface IProps { + isOpen: boolean; + onCreate: (transaction: any) => void; + } + + let { isOpen = $bindable(false), onCreate = () => {} }: IProps = $props(); // Changed default to false + + let amount: number = $state(0); + let currency: string = $state('USD'); + let label: string = $state(''); + let receiverLabel: string = $state(''); + let receiverIban: string = $state(''); + let operationDate: string = $state(''); + + function closeDialog() { + isOpen = false; + } + + async function createTransaction(event: Event) { + event.preventDefault(); + + const transaction = { + amount, + currency, + label, + receiverLabel, + receiverIban, + operationDate + }; + + try { + const response = await authFetch('/api/transactions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(transaction) + }); + + if (!response.ok) { + const error = await response.json(); + alert('Erreur lors de la création de la transaction: ' + error); + return; + } + + onCreate(await response.json()); + + closeDialog(); + } catch (err) { + alert('Erreur réseau lors de la création de la transaction'); + } + } +</script> + +<!-- svelte-ignore a11y_click_events_have_key_events --> +<!-- svelte-ignore a11y_no_static_element_interactions --> +{#if isOpen} + <div class="backdrop" onclick={closeDialog}> + <dialog open onclick={(e) => e.stopPropagation()}> + <h2>Créer une nouvelle transaction</h2> + <form class="flex flex-col gap-2"> + <label> + <span>Montant</span> + <input type="number" step="0.01" min="0" bind:value={amount} required /> + </label> + <label> + <span>Devise</span> + <select bind:value={currency} required> + <option selected value="USD" data-symbol="$" data-name="Dollar américain" + >USD - Dollar américain</option + > + <option value="EUR" data-symbol="€" data-name="Euro">EUR - Euro</option> + <option value="GBP" data-symbol="£" data-name="Livre sterling" + >GBP - Livre sterling</option + > + <option value="JPY" data-symbol="¥" data-name="Yen japonais">JPY - Yen japonais</option> + <option value="CHF" data-symbol="Fr" data-name="Franc suisse">CHF - Franc suisse</option + > + <option value="CAD" data-symbol="$" data-name="Dollar canadien" + >CAD - Dollar canadien</option + > + <option value="AUD" data-symbol="$" data-name="Dollar australien" + >AUD - Dollar australien</option + > + <option value="CNY" data-symbol="¥" data-name="Yuan chinois">CNY - Yuan chinois</option> + </select> + </label> + <label> + <span>Libellé</span> + <input type="text" bind:value={label} required /> + </label> + <label> + <span>Libellé du bénéficiaire</span> + <input type="text" bind:value={receiverLabel} required /> + </label> + <label> + <span>IBAN du bénéficiaire</span> + <input type="text" bind:value={receiverIban} required /> + </label> + <label> + <span>Date de l'opération</span> + <input type="datetime-local" bind:value={operationDate} required /> + </label> + <button class="btn" onclick={createTransaction}>➕ Créer</button> + </form> + </dialog> + </div> +{/if} + +<style> + .backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + dialog { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 90vw; + max-height: 90vh; + border: none; + border-radius: 8px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2); + padding: 2rem; + background: var(--bg-primary); + outline: none; + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; + justify-content: center; + color: white; + } + + h2 { + font-weight: 600; + font-size: 32px; + color: var(--text-lime); + } + + form { + display: flex; + flex-direction: column; + gap: 8px; + } + + label { + display: flex; + flex-direction: column; + } +</style> diff --git a/ping/frontend/src/lib/components/input/StockSelector.svelte b/ping/frontend/src/lib/components/input/StockSelector.svelte new file mode 100644 index 0000000..1237128 --- /dev/null +++ b/ping/frontend/src/lib/components/input/StockSelector.svelte @@ -0,0 +1,231 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import Button from '../Button.svelte'; + + let { selectedStock = $bindable('AAPL') } = $props(); + + let stocks: { + symbol: string; + shortname: string; + quoteType: string; + isYahooFinance: boolean; + }[] = $state([]); + let news: { + link: string; + title: string; + thumbnail: { resolutions: { url: string; width: number; height: number }[] }; + }[] = $state([]); + + function openDialog() { + const dialog = document.querySelector('dialog'); + const backdrop = document.getElementById('backdrop'); + // @ts-ignore + dialog.open = true; + // @ts-ignore + backdrop.style.display = 'block'; + } + + function closeDialog() { + const dialog = document.querySelector('dialog'); + const backdrop = document.getElementById('backdrop'); + // @ts-ignore + dialog.open = false; + // @ts-ignore + backdrop.style.display = 'none'; + } + + function onSearch(event: SubmitEvent) { + event.preventDefault(); + // @ts-ignore + const fd = new FormData(event.target); + const searchQuery = fd.get('search'); + + fetch(`/stocksapi/search?query=${searchQuery}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + } + }) + .then((response) => response.json()) + .then((data) => { + stocks = data.quotes.filter((q: any) => q.isYahooFinance); + news = data.news; + }) + .catch((error) => { + console.error('Error fetching stock data:', error); + }); + } + + onMount(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + closeDialog(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }); +</script> + +<Button onclick={openDialog}>{selectedStock}</Button> + +<dialog> + <form onsubmit={onSearch}> + <input + type="search" + name="search" + id="search" + placeholder="Recherche d'actions, ETFs, entreprises" + value={selectedStock || ''} + /> + <button type="submit" class="btn">Rechercher</button> + </form> + <div class="results"> + <div class="stocks"> + {#if stocks.length === 0} + <p>Aucune action, ETFs trouvés.</p> + {:else} + <p>{stocks.length} actions, ETFs trouvés</p> + {#each stocks as stock} + <button + class="btn stock" + onclick={() => { + selectedStock = stock.symbol; + closeDialog(); + }} + disabled={!stock.isYahooFinance} + > + <pre>{stock.symbol}</pre> + <span>{stock.shortname}</span> + <i>{stock.quoteType}</i> + </button> + {/each} + {/if} + </div> + <div class="news"> + {#if news.length === 0} + <p>Pas de news trouvés.</p> + {:else} + <p>{news.length} news trouvées</p> + {#each news as newsItem} + <a class="newsItem" href={newsItem.link}> + {#if newsItem.thumbnail?.resolutions?.length > 0} + <img + src={newsItem.thumbnail.resolutions[0].url} + alt="News Thumbnail" + width={newsItem.thumbnail.resolutions[0].width} + height={newsItem.thumbnail.resolutions[0].height} + /> + {/if} + <h1>{newsItem.title}</h1></a + > + {/each} + {/if} + </div> + </div> +</dialog> +<!-- svelte-ignore a11y_click_events_have_key_events --> +<!-- svelte-ignore a11y_no_static_element_interactions --> +<div id="backdrop" onclick={closeDialog}></div> + +<style> + dialog { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + padding: 20px; + background-color: var(--bg-primary); + border: 4px solid #ccc; + border-radius: 16px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + height: 90vh; + width: 90vw; + z-index: 1000; + color: white; + } + + #backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; + display: none; + } + + form { + display: flex; + gap: 10px; + } + + input[type='search'] { + width: 300px; + } + + .results { + display: flex; + flex-direction: row; + gap: 20px; + justify-content: space-evenly; + } + + .stocks, + .news { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + max-height: 70vh; + padding: 16px; + flex: 1; + } + + .stock { + display: flex; + align-items: center; + gap: 8px; + } + + .stock pre { + font-weight: bold; + padding: 8px; + background: var(--bg-primary); + border-radius: 8px; + } + + .stock span { + font-weight: bold; + color: var(--text-lime); + } + + .newsItem { + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + padding-bottom: 4px; + border-radius: 8px; + transition-duration: 0.2s; + } + + .newsItem h1 { + margin: 16px; + } + + .newsItem img { + border-radius: 8px; + width: 100%; + height: auto; + } + + .newsItem:hover { + transform: scale(1.025); + background-color: #333; + } +</style> diff --git a/ping/frontend/src/lib/components/input/UserSelector.svelte b/ping/frontend/src/lib/components/input/UserSelector.svelte new file mode 100644 index 0000000..a0c80c7 --- /dev/null +++ b/ping/frontend/src/lib/components/input/UserSelector.svelte @@ -0,0 +1,35 @@ +<script lang="ts"> + import type { IUser } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import Avatar from '../Avatar.svelte'; + + let { users = $bindable<IUser[]>([]) }: { users: IUser[] } = $props(); +</script> + +<div> + {#each users as u, i} + <Avatar + username={u.displayName} + url={u.avatar} + onclick={() => { + users = users.filter((_, index) => index !== i); + }} + /> + {/each} + <Avatar + username={'Ajouter un utilisateur'} + url={'/icons/add-green.svg'} + onclick={() => { + let userId = prompt("Entrez l'ID de l'utilisateur à ajouter :"); + if (userId === null || userId.trim() === '') { + addToast({ title: 'ID utilisateur invalide.' }); + return; + } + if (users.map((u) => u.displayName).includes(userId)) { + addToast({ title: 'Cet utilisateur est déjà ajouté.' }); + return; + } + users = [...users, { displayName: userId, avatar: '/img/default-avatar.png' } as IUser]; + }} + /> +</div> |
