Convert backend/web from submodule to regular directory

This commit is contained in:
2026-08-08 23:26:14 +02:00
parent 102a316d42
commit 0010c92103
89 changed files with 6559 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
import type { User, Session } from 'better-auth';
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
interface Locals { user?: User; session?: Session }
// interface Error {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+62
View File
@@ -0,0 +1,62 @@
import { goto } from "$app/navigation";
import { getToken, logout } from "$lib/state/auth.svelte"
const BASE_URL = import.meta.env.VITE_API_URL;
interface RequestOptions extends RequestInit {
auth?: boolean; // true par défaut
}
class ApiError extends Error {
constructor(public status: number, public body: string) {
super(`API Error ${status}`);
}
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { auth = true, headers, ...rest } = options;
const requestHeaders: HeadersInit = {
'Content-Type': 'application/json',
...headers,
};
const fetchHeaders = new Headers(requestHeaders)
if (auth) {
const token = getToken()
if (token) {
fetchHeaders.append('Authorization', `Bearer ${token}`)
}
}
const res = await fetch(`${BASE_URL}${path}`, {
...rest,
headers: fetchHeaders,
});
if (res.status === 401 ) {
logout()
goto("/login")
//throw new ApiError(401, await safeJson(res));
}
if (!res.ok) {
throw new ApiError(res.status, await safeJson(res));
}
if (res.status === 204) return undefined as T;
return res.json();
}
async function safeJson(res: Response) {
try { return await res.json(); } catch { return null; }
}
export const api = {
get: <T>(path: string, opts?: RequestOptions) => request<T>(path, { ...opts, method: 'GET' }),
post: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
request<T>(path, { ...opts, method: 'POST', body: JSON.stringify(body) }),
put: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
request<T>(path, { ...opts, method: 'PUT', body: JSON.stringify(body) }),
delete: <T>(path: string, opts?: RequestOptions) => request<T>(path, { ...opts, method: 'DELETE' }),
};
+33
View File
@@ -0,0 +1,33 @@
import { api } from '../client';
import { type User } from "$lib/types/api"
export interface Credentials {
email: string
password: string
}
export interface UserReponse {
user : User
accessToken: string
refreshToken: string
expiresAt: Date
}
export interface RegistrationData {
method: string // "password"
email: string
firstname: string
lastname: string
password: string
}
export const authApi = {
register: (data: RegistrationData) => api.post<User>('/auth/register', data),
status: () => api.get<User>('/auth/status'),
login: (data: Credentials) => api.post<UserReponse>('/auth/login', data),
// getAll: () => api.get<User[]>('/users'),
// getById: (id: string) => api.get<User>(`/users/${id}`),
// create: (data: Partial<User>) => api.post<User>('/users', data),
// update: (id: string, data: Partial<User>) => api.put<User>(`/users/${id}`, data),
// remove: (id: string) => api.delete<void>(`/users/${id}`),
};
@@ -0,0 +1,7 @@
import { api } from '../client';
import { type User } from "$lib/types/api"
export const userApi = {
me: () => api.get<User>('/user/me'),
update: (id: string, data: Partial<User>) => api.put<User>(`/user/${id}`, data),
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="13mm"
height="13mm"
viewBox="0 0 13 13"
version="1.1"
id="svg1"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="favicon.svg"
inkscape:export-filename="../backend/web/src/lib/assets/logo-txt.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.4431818"
inkscape:cx="287.90552"
inkscape:cy="214.80315"
inkscape:window-width="2560"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-size:14.1111px;font-family:pixelart;-inkscape-font-specification:pixelart;writing-mode:lr-tb;direction:ltr;fill:#37abc8;stroke-width:0.264583"
x="2.7499995"
y="11.549997"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.1111px;font-family:'GohuFont 11 Nerd Font';-inkscape-font-specification:'GohuFont 11 Nerd Font';fill:#0ea5e9;fill-opacity:1;stroke-width:0.264583"
x="2.7499995"
y="11.549997">l</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,13 @@
<script lang="ts">
type Props = {
errors: Map<string,string>
key: string
}
let { errors, key }: Props = $props();
</script>
{#if errors.has(key)}
<span class="text-red-700 dark:text-red-400 text-xs">{errors.get(key)}</span>
{:else}
<span class="text-xs">&nbsp;</span>
{/if}
@@ -0,0 +1,13 @@
<script lang="ts">
import {uiState} from "$lib/state/ui.svelte"
let {label='', icon='', link=''} = $props();
</script>
<div class="my-2 block">
<a class="p-2 w-full text-left menuitem block" href={link}>
<i class={[icon]}></i>
{#if uiState.menuContentVisible}
<span class="ml-4">{label}</span>
{/if}
</a>
</div>
@@ -0,0 +1,55 @@
<script lang="ts">
import { uiState } from "$lib/state/ui.svelte"
import { auth } from "$lib/state/auth.svelte"
import Menu from "$lib/components/Menu.svelte"
import { logout } from "$lib/state/auth.svelte"
import logo from "$lib/assets/logo-txt.png"
let {handleMenuDesktop, handleMenuMobile} = $props();
const handleLogout = () => {
logout()
}
</script>
<div class="flex flex-row italic mb-4">
{#if uiState.menuContentVisible}
<div class="flex-1 p-3 font openonly">
<img src={logo} class="h-5" alt="logo"/>
</div>
{/if}
<button id="hamburgerDesktop" onclick={handleMenuDesktop} title="Open menu">
<i class="icon-menu pointer block p-2"></i>
</button>
<button id="hamburgerMobile" onclick={handleMenuMobile} title="Open menu">
<i class="icon-menu pointer block p-2"></i>
</button>
</div>
<div id="menu" class="flex-1 overflow-y-scroll">
<Menu label="Conversation" icon="icon-chat" link="/"/>
<Menu label="Tâches" icon="icon-tasks" link="/tasks"/>
<Menu label="Connaissances" icon="icon-graduation-cap" link="/kb"/>
<Menu label="Historique" icon="icon-history" link="/history"/>
{#if auth.user?.administrator}
<Menu label="Paramétrage" icon="icon-cog" link="/settings"/>
{/if}
</div>
<div id="profile" class="w-full flex flex-row items-center border-t border-stone-200 dark:border-stone-800">
{#if uiState.menuContentVisible}
<div class="p-4 flex-1 profile">
<div>Fabien Masson</div>
<div class="flex">
<a href="/profile" class="text-xs flex-1">Modifier</a>
</div>
</div>
<div class="p-2">
<button class="w-full bt" onclick={handleLogout} title="Déconecter">
<i class="icon-logout"></i>
</button>
</div>
{:else}
<Menu label="User" icon="icon-user" link="/profile"/>
{/if}
</div>
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+26
View File
@@ -0,0 +1,26 @@
import { type User } from "$lib/types/api"
type AuthState = {
user: User | null;
status: 'loading' | 'authenticated' | 'unauthenticated';
//accessToken: string | null
}
export const auth: AuthState = $state({
user: null,
status: 'loading',
//accessToken: null
})
export const logout = () => {
localStorage.removeItem("accessToken")
auth.status = 'unauthenticated'
}
export const getToken = (): string | null => {
return localStorage.getItem("accessToken")
}
export const setToken = (token : string) => {
localStorage.setItem("accessToken", token)
}
@@ -0,0 +1,20 @@
let messageIndex = 0
interface Message {
id : number
type: 'success' | 'failure'
text: string,
status: 'appearing' | 'visible' | 'disappearing'
}
export const messages: Array<Message> = $state([])
export const putMessage = (type: 'success' | 'failure', text: string)=> {
messages.push({
id: messageIndex++,
type: type,
text: text,
status: 'appearing'
})
}
+5
View File
@@ -0,0 +1,5 @@
export const uiState = $state({
sidebarDesktopOpen: true,
sidebarMobileOpen: false,
menuContentVisible: true
});
+10
View File
@@ -0,0 +1,10 @@
export interface User {
id: string
firstname: string
lastname: string
email: string
administrator: boolean
theme: string
lang: string
_version: string
}
+17
View File
@@ -0,0 +1,17 @@
export const applyTheme = () => {
document.documentElement.classList.toggle(
"dark",
localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
);
}
export const themeToLocalStorage = (theme: string) => {
if (theme=='light') {
localStorage.setItem('theme','light')
} else if (theme=='dark') {
localStorage.setItem('theme','dark')
} else {
localStorage.removeItem('theme')
}
}
@@ -0,0 +1,5 @@
const emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export function validateEmail(input: string): boolean {
return emailRegex.test(input);
}
@@ -0,0 +1,7 @@
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
@@ -0,0 +1,3 @@
export function load() {
return { title: 'Conversation' };
}
@@ -0,0 +1,80 @@
<script lang="ts">
import type { LayoutProps } from './$types';
import { page } from '$app/state';
import {uiState} from "$lib/state/ui.svelte"
import { auth } from '$lib/state/auth.svelte'
import Sidebar from "$lib/components/Sidebar.svelte"
import { goto } from '$app/navigation'
import { onMount } from "svelte";
import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher';
let { data, children }: LayoutProps = $props();
const handleMenuDesktop = () => {
uiState.sidebarDesktopOpen = !uiState.sidebarDesktopOpen
if (uiState.sidebarDesktopOpen) {
setTimeout(() => { uiState.menuContentVisible = uiState.sidebarDesktopOpen }, 200)
} else {
uiState.menuContentVisible = uiState.sidebarDesktopOpen
}
}
const handleMenuMobile = () => {
uiState.sidebarMobileOpen = !uiState.sidebarMobileOpen
if (uiState.sidebarMobileOpen) {
setTimeout(() => { uiState.menuContentVisible = uiState.sidebarMobileOpen }, 200)
} else {
uiState.menuContentVisible = uiState.sidebarMobileOpen
}
}
const handleResize = () => {
if (window.innerWidth < 640) {
uiState.menuContentVisible = uiState.sidebarMobileOpen
} else {
uiState.menuContentVisible = uiState.sidebarDesktopOpen
}
}
onMount(async () => {
handleResize()
if (data.me) {
auth.user = data.me
themeToLocalStorage(auth.user.theme)
applyTheme()
auth.status = 'authenticated'
} else {
auth.status = 'unauthenticated'
}
})
$effect(() => {
if (auth.status == 'unauthenticated') {
goto("/login")
}
});
</script>
<svelte:window onresize={handleResize} />
<div id="outer" class={[uiState.sidebarDesktopOpen ? "desktopMenuOpen" : "desktopMenuClosed", uiState.sidebarMobileOpen ? "mobileMenuOpen" : "mobileMenuClosed" ]}>
<div id="sidebar" class="flex flex-col">
<Sidebar {handleMenuDesktop} {handleMenuMobile}/>
</div>
<div id="main">
<div class="flex flex-col h-screen ">
<div class={["header", page.data.theme]}>
<h1 class="flex">
<span class="flex-1">{page.data.title}</span>
{#if page.data.back}
<span><a href={page.data.back} title="retour"><i class="icon-left-big"></i></a></span>
{/if}
</h1>
</div>
<div class="overflow-scroll flex-1 p-2">
{@render children()}
</div>
</div>
</div>
</div>
@@ -0,0 +1,8 @@
import { userApi } from "$lib/api/endpoints/user"
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
return {
me: await userApi.me()
}
}
@@ -0,0 +1,6 @@
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
@@ -0,0 +1,3 @@
export function load() {
return { title: 'Historique' };
}
@@ -0,0 +1,3 @@
export function load() {
return { title: 'Base de connaissance' };
}
@@ -0,0 +1,109 @@
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import type { PageProps } from './$types';
import { type User } from '$lib/types/api'
import { userApi } from '$lib/api/endpoints/user'
import FormError from '$lib/components/FormError.svelte';
import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher';
import { validateEmail } from '$lib/utilities/validators'
let { data }: PageProps = $props();
let form : User = $state(data.me)
let errors = new SvelteMap<string,string>()
$effect(()=>{
themeToLocalStorage(form.theme)
applyTheme()
})
let submitUser = (e: Event) => {
e.preventDefault()
if (!validateEmail(form.email)) {
errors.set("email","Adresse email invalide !")
}
if (!form.firstname) {
errors.set("firstname","Le prénom est obligatoire !")
}
if (!form.lastname) {
errors.set("lastname","Le nom est obligatoire !")
}
if (errors.size==0) {
userApi.update(form.id, form)
.then(()=>{ console.log('success')})
.catch((err: string)=>{ console.error(err)})
}
}
</script>
<div class="mb-4 formgroup">
<h2>Mon compte</h2>
<form onsubmit={submitUser} onchange={()=>{errors.clear()}}>
<label for="email" class="block my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
<div class="flex">
<div class="flex-1 mr-2">
<label for="firstname" class="block my-1 flex-1">Prénom</label>
<input
id="firstname"
type="text"
bind:value={form.firstname}
class="textinput"
/>
<FormError key="firstname" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lastname" class="block my-1 flex-1">Nom</label>
<input
id="lastname"
type="text"
bind:value={form.lastname}
class="textinput"
/>
<FormError key="lastname" errors={errors} />
</div>
</div>
<div class="flex">
<div class="flex-1 mr-2">
<label for="theme" class="block my-1 flex-1">Thème</label>
<select bind:value={form.theme} class="textinput">
<option value="auto">Automatic</option>
<option value="dark">Sombre</option>
<option value="light">Clair</option>
</select>
<FormError key="theme" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lang" class="block my-1 flex-1">Langue</label>
<select bind:value={form.lang} class="textinput">
<option value="en">English</option>
<option value="fr">Français</option>
</select>
<FormError key="lang" errors={errors} />
</div>
</div>
<button class="bt w-full">Enregistrer</button>
</form>
</div>
<div class="mb-4 formgroup">
<h2>Canaux de communication</h2>
</div>
@@ -0,0 +1,9 @@
import { userApi } from "$lib/api/endpoints/user"
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return {
title: 'Profil',
me: await userApi.me()
}
}
@@ -0,0 +1,58 @@
<div class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<a class="bt p-5 text-center"
title="Modèles"
href="/settings/models">
Modèles
<span class="ml-2 opacity-50 text-xs">mistral-medium-latest</span>
</a>
<a class="bt p-5 text-center"
title="Outils"
href="/settings/tools"
>
Outils
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">12</span>
</a>
<a class="bt p-5 text-center"
title="Skills"
href="/settings/skills">
Skills
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">3</span>
</a>
<a class="bt p-5 text-center"
title="Canaux de communication"
href="/settings/channels"
>
Canaux de communication
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">12</span>
</a>
<a class="bt p-5 text-center"
title="Bases de connaissance"
href="/settings/kb">
Bases de connaissance
</a>
<a class="bt p-5 text-center"
title="Agents"
href="/settings/agents">
Agents
</a>
<a class="bt p-5 text-center"
title="Mémoire"
href="/settings/memory">
Mémoire
<span class="ml-2 opacity-50 text-xs">standard</span>
</a>
<a class="bt p-5 text-center"
title="Utilisateurs"
href="/settings/users">
Utilisateurs
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">1</span>
</a>
</div>
@@ -0,0 +1,3 @@
export function load() {
return { title: 'Paramétrage', theme: "settings" };
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Agents',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Canaux de communication',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Base de connaissance',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Mémoire',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Modèles',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Skills',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Outils',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1 @@
<div>Modèles</div>
@@ -0,0 +1,7 @@
export function load() {
return {
title: 'Paramétrage : Utilisateurs',
back: "/settings",
theme: "settings"
};
}
@@ -0,0 +1,3 @@
<div>
Tâches
</div>
@@ -0,0 +1,3 @@
export function load() {
return { title: 'Tâches' };
}
@@ -0,0 +1,105 @@
<script lang="ts">
import logo from "$lib/assets/logo-txt.png"
import { auth, setToken } from '$lib/state/auth.svelte';
import { goto } from '$app/navigation';
import { authApi } from "$lib/api/endpoints/auth"
type Credentials = {
email: string
validemail: boolean
password : string
}
let credentials: Credentials = $state({
email: "",
password: "",
validemail: false
})
const formSubmitEmail = () => {
credentials.validemail = true
}
const formSubmitPass = (e: Event) => {
e.preventDefault()
authApi.login(credentials)
.then((res)=>{
auth.user = res.user
setToken(res.accessToken)
goto("/")
})
.catch((err)=>{
console.log(err)
})
}
</script>
<div class="h-full w-full flex items-center public">
<div class="m-auto w-96 max-w-full p-4">
<img src={logo} class="h-10 mb-4" alt="logo"/>
<h1>Identifiez-vous ...</h1>
{#if !credentials.validemail}
<form onsubmit={formSubmitEmail}>
<div class="flex">
<label for="email" class="block mb-2 flex-1">Email</label>
<a href="/recover-password" class="text-xs">Mot de passe oublié ?</a>
</div>
<div class="flex flex-row">
<input
type="text"
bind:value={credentials.email}
class="textinput mr-1"
autofocus
/>
<button class="bt" title="page suivante">
<i class="icon-right-big"></i>
</button>
</div>
</form>
<div class="my-4 text-xs">
<a href="/register">Pas encore de compte ? Incrivez-vous.</a>
</div>
{:else}
<form onsubmit={formSubmitPass}>
<div class="flex">
<label for="email" class="block mb-2 flex-1">Mot de passe</label>
</div>
<div class="flex flex-row">
<input
type="password"
bind:value={credentials.password}
class="textinput mr-1"
autofocus
/>
<button class="bt">
Entrer
</button>
</div>
<a href="/login" class="text-xs" onclick={()=>{credentials.validemail=false}}> Retour</a>
</form>
{/if}
<hr class="my-4">
<div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-4">
<button class="bt p-5 text-center" title="Connexion avec Google">
<i class="icon-google"></i>
</button>
<button class="bt p-5 text-center" title="Connexion avec Facebook">
<i class="icon-facebook"></i>
</button>
<button class="bt p-5 text-center" title="Connexion avec Github">
<i class="icon-github"></i>
</button>
<button class="bt p-5 text-center" title="Connexion avec Apple">
<i class="icon-apple"></i>
</button>
</div>
</div>
</div>
@@ -0,0 +1,120 @@
<script lang="ts">
import logo from "$lib/assets/logo-txt.png"
import { SvelteMap } from 'svelte/reactivity';
import { goto } from '$app/navigation';
import { validateEmail } from '$lib/utilities/validators'
import FormError from "$lib/components/FormError.svelte";
import { authApi } from "$lib/api/endpoints/auth"
type RegisterForm = {
email: string
firstname: string
lastname: string
password : string
verifPassword: string
}
let errors = new SvelteMap<string,string>()
let form: RegisterForm = $state({
email: "",
firstname: "",
lastname: "",
password: "",
verifPassword: ""
})
const handleSubmit = (e: Event) => {
e.preventDefault()
errors.clear()
if (!validateEmail(form.email)) {
errors.set("email","Adresse email invalide !")
}
if (form.password!==form.verifPassword) {
errors.set("verifPassword","Les mots de pass ne correspondent pas.")
}
if (errors.size==0) {
let data = {
method: "password",
email: form.email,
firstname: form.firstname,
lastname: form.lastname,
password: form.password
}
authApi.register(data)
.then(()=>{ goto("/") })
.catch((reason: string)=>{ errors.set("submit","Erreur: "+reason) })
}
}
</script>
<div class="h-full w-full flex items-center public">
<div class="m-auto w-96 max-w-full p-4">
<img src={logo} class="h-10 mb-4" alt="logo"/>
<h1>Inscription</h1>
<form onsubmit={handleSubmit} onchange={()=>{errors.clear()}}>
<label for="email" class="block my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
<label for="firstname" class="block my-1 flex-1">Prénom</label>
<input
id="firstname"
type="text"
bind:value={form.firstname}
class="textinput"
/>
<FormError key="firstname" {errors} />
<label for="lastname" class="block my-1 flex-1">Nom</label>
<input
id="lastname"
type="text"
bind:value={form.lastname}
class="textinput"
/>
<FormError key="lastname" {errors} />
<label for="password" class="block my-1 flex-1">Mot de passe</label>
<input
id="password"
type="password"
bind:value={form.password}
class="textinput"
/>
<FormError key="password" {errors} />
<label for="verifpassword" class="block my-1 flex-1">Vérification du mot de passe</label>
<input
id="verifpassword"
type="password"
bind:value={form.verifPassword}
class="textinput"
/>
<FormError key="verifPassword" {errors} />
<button class="bt my-2 w-full ">
Enregistrer
</button>
<FormError key="submit" {errors} />
</form>
</div>
</div>
+2
View File
@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
import { applyTheme } from '$lib/ui/themeswitcher';
import { onMount } from 'svelte';
let { children } = $props();
onMount(()=>{ applyTheme() })
</script>
<svelte:head>
<title>Lasssistanoque</title>
<link rel="icon" href={favicon} />
<link rel="stylesheet" href="/assets/fontello/css/fontello.css">
</svelte:head>
{@render children()}
+107
View File
@@ -0,0 +1,107 @@
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:where(.dark, .dark *));
html {
@apply h-full;
}
body {
@apply h-full bg-stone-100 text-stone-900 dark:bg-stone-900 dark:text-stone-200;
}
label {
@apply text-xs text-stone-600 uppercase dark:text-stone-400;
}
hr {
@apply border-stone-200 dark:border-stone-800;
}
h2 {
@apply mb-4 font-bold;
}
.textinput {
@apply w-full rounded bg-white p-1 text-stone-900 dark:bg-stone-700 dark:text-stone-100;
}
.pointer {
@apply cursor-pointer;
}
.bt {
@apply cursor-pointer rounded bg-stone-200 px-4 py-2 hover:bg-stone-300 dark:bg-stone-800 dark:hover:bg-stone-700;
}
.menuitem {
@apply text-stone-800 text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;
}
.header {
@apply p-2 text-lg;
h1 {
@apply rounded bg-stone-200 px-2 font-bold text-sky-600 dark:bg-stone-800 dark:text-sky-200;
}
&.settings {
h1 {
@apply rounded border-none bg-sky-200 px-2 text-sky-700 dark:bg-sky-950 dark:text-sky-300;
}
}
}
.public h1 {
@apply mb-4 text-3xl font-bold;
}
.profile a,
.public a {
@apply text-sky-500;
}
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 dark:border-stone-700 dark:bg-stone-900;
}
#outer {
@apply flex h-full flex-row;
}
#main {
@apply flex w-full flex-col;
}
#sidebar {
@apply min-w-64 border-r border-r-stone-200 transition-all duration-300 dark:border-stone-800;
}
#content {
@apply p-2;
}
#hamburgerDesktop,
#hamburgerMobile {
@apply hidden;
}
@media (width >= 640px) {
#hamburgerDesktop {
@apply inline;
}
.desktopMenuClosed {
#sidebar {
@apply w-10 min-w-10;
}
}
}
@media (width < 640px) {
#hamburgerMobile {
@apply inline;
}
#main {
@apply ml-10;
}
#sidebar {
@apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-900;
}
.mobileMenuClosed {
#sidebar {
@apply fixed w-10 min-w-10;
}
}
}