Convert backend/web from submodule to regular directory
@@ -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 {};
|
||||
@@ -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>
|
||||
@@ -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' }),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 533 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -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 |
|
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"> </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>
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const uiState = $state({
|
||||
sidebarDesktopOpen: true,
|
||||
sidebarMobileOpen: false,
|
||||
menuContentVisible: true
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface User {
|
||||
id: string
|
||||
firstname: string
|
||||
lastname: string
|
||||
email: string
|
||||
administrator: boolean
|
||||
theme: string
|
||||
lang: string
|
||||
_version: string
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
@@ -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()}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||