Merge pull request #17 from Djeeberjr/feature/idmapping

Feature/idmapping
This commit is contained in:
Psenfft 2025-05-20 16:40:52 +02:00 committed by GitHub
commit 53584fee24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1472 additions and 384 deletions

1
Cargo.lock generated
View File

@ -1116,6 +1116,7 @@ dependencies = [
"rocket_codegen", "rocket_codegen",
"rocket_http", "rocket_http",
"serde", "serde",
"serde_json",
"state", "state",
"tempfile", "tempfile",
"time", "time",

View File

@ -13,7 +13,7 @@ gpio = "0.4.1"
regex = "1.11.1" regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
rocket = "0.5.1" rocket = { version = "0.5.1", features = ["json"] }
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", features = ["full"] }
rust-embed = "8.7.0" rust-embed = "8.7.0"
log = "0.4.27" log = "0.4.27"

76
src/id_mapping.rs Normal file
View File

@ -0,0 +1,76 @@
use crate::tally_id::TallyID;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct Name {
pub first: String,
pub last: String,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct IDMapping {
id_map: HashMap<TallyID, Name>,
}
impl IDMapping {
pub fn new() -> Self {
IDMapping {
id_map: HashMap::new(),
}
}
pub fn map(&self, id: &TallyID) -> Option<&Name> {
self.id_map.get(id)
}
pub fn add_mapping(&mut self, id: TallyID, name: Name) {
self.id_map.insert(id, name);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let mut map = IDMapping::new();
let id1 = TallyID("A2Fb44".to_owned());
let name1 = Name {
first: "Max".to_owned(),
last: "Mustermann".to_owned(),
};
map.add_mapping(id1.clone(), name1.clone());
let res = map.map(&id1);
assert_eq!(res, Some(&name1));
}
#[test]
fn multiple() {
let mut map = IDMapping::new();
let id1 = TallyID("A2Fb44".to_owned());
let name1 = Name {
first: "Max".to_owned(),
last: "Mustermann".to_owned(),
};
let id2 = TallyID("7D3DF5B5".to_owned());
let name2 = Name {
first: "First".to_owned(),
last: "Last".to_owned(),
};
map.add_mapping(id1.clone(), name1.clone());
map.add_mapping(id2.clone(), name2.clone());
let res = map.map(&id1);
assert_eq!(res, Some(&name1));
let res = map.map(&id2);
assert_eq!(res, Some(&name2));
}
}

View File

@ -5,7 +5,7 @@ use std::{
}; };
use tokio::fs; use tokio::fs;
use crate::tally_id::TallyID; use crate::{id_mapping::IDMapping, tally_id::TallyID};
/// Represents a single day that IDs can attend /// Represents a single day that IDs can attend
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -18,12 +18,14 @@ pub struct AttendanceDay {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct IDStore { pub struct IDStore {
days: HashMap<String, AttendanceDay>, days: HashMap<String, AttendanceDay>,
pub mapping: IDMapping,
} }
impl IDStore { impl IDStore {
pub fn new() -> Self { pub fn new() -> Self {
IDStore { IDStore {
days: HashMap::new(), days: HashMap::new(),
mapping: IDMapping::new(),
} }
} }
@ -82,10 +84,18 @@ impl IDStore {
days.sort(); days.sort();
let header = days.join(seperator); let header = days.join(seperator);
csv.push_str(&format!("ID{}{}\n", seperator, header)); csv.push_str(&format!(
"ID{seperator}Nachname{seperator}Vorname{seperator}{header}\n"
));
for user_id in user_ids.iter() { for user_id in user_ids.iter() {
csv.push_str(&user_id.0.to_string()); let id = &user_id.0.to_string();
let name = self.mapping.map(user_id);
let firstname = name.map(|e| e.first.clone()).unwrap_or("".to_owned());
let lastname = name.map(|e| e.last.clone()).unwrap_or("".to_owned());
csv.push_str(&format!("{id}{seperator}{lastname}{seperator}{firstname}"));
for day in days.iter() { for day in days.iter() {
let was_there: bool = self let was_there: bool = self
.days .days
@ -95,7 +105,7 @@ impl IDStore {
.contains(user_id); .contains(user_id);
if was_there { if was_there {
csv.push_str(&format!("{}x", seperator)); csv.push_str(&format!("{seperator}x"));
} else { } else {
csv.push_str(seperator); csv.push_str(seperator);
} }

View File

@ -10,7 +10,7 @@ use std::{env, error::Error, sync::Arc};
use tally_id::TallyID; use tally_id::TallyID;
use tokio::{ use tokio::{
fs, join, fs, join,
sync::{Mutex, mpsc}, sync::{Mutex, broadcast, mpsc},
}; };
use webserver::start_webserver; use webserver::start_webserver;
@ -20,6 +20,7 @@ use mock::{MockBuzzer, MockHotspot, MockLed};
mod buzzer; mod buzzer;
mod color; mod color;
mod hotspot; mod hotspot;
mod id_mapping;
mod id_store; mod id_store;
mod led; mod led;
mod mock; mod mock;
@ -138,7 +139,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
info!("Starting application"); info!("Starting application");
let (tx, mut rx) = mpsc::channel::<String>(32); let (tx, mut rx) = broadcast::channel::<String>(32);
let sse_tx = tx.clone();
tokio::spawn(async move { tokio::spawn(async move {
match run_pm3(tx).await { match run_pm3(tx).await {
@ -178,7 +180,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let channel_store = store.clone(); let channel_store = store.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(tally_id_string) = rx.recv().await { while let Ok(tally_id_string) = rx.recv().await {
let tally_id = TallyID(tally_id_string); let tally_id = TallyID(tally_id_string);
if hotspot_ids.contains(&tally_id) { if hotspot_ids.contains(&tally_id) {
@ -203,7 +205,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
} }
}); });
match start_webserver(store.clone()).await { match start_webserver(store.clone(), sse_tx).await {
Ok(()) => {} Ok(()) => {}
Err(e) => { Err(e) => {
error!("Failed to start webserver: {e}"); error!("Failed to start webserver: {e}");

View File

@ -4,12 +4,12 @@ use std::error::Error;
use std::process::Stdio; use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::mpsc; use tokio::sync::broadcast;
/// Runs the pm3 binary and monitors it's output /// Runs the pm3 binary and monitors it's output
/// The pm3 binary is ether set in the env var PM3_BIN or found in the path /// The pm3 binary is ether set in the env var PM3_BIN or found in the path
/// The ouput is parsed and send via the `tx` channel /// The ouput is parsed and send via the `tx` channel
pub async fn run_pm3(tx: mpsc::Sender<String>) -> Result<(), Box<dyn Error>> { pub async fn run_pm3(tx: broadcast::Sender<String>) -> Result<(), Box<dyn Error>> {
let pm3_path = match env::var("PM3_BIN") { let pm3_path = match env::var("PM3_BIN") {
Ok(path) => path, Ok(path) => path,
Err(_) => { Err(_) => {
@ -35,7 +35,7 @@ pub async fn run_pm3(tx: mpsc::Sender<String>) -> Result<(), Box<dyn Error>> {
while let Some(line) = reader.next_line().await? { while let Some(line) = reader.next_line().await? {
trace!("PM3: {line}"); trace!("PM3: {line}");
if let Some(uid) = super::parser::parse_line(&line) { if let Some(uid) = super::parser::parse_line(&line) {
tx.send(uid).await?; tx.send(uid)?;
} }
} }

View File

@ -1,21 +1,36 @@
use log::{error, info, warn}; use log::{error, info, warn};
use rocket::http::Status; use rocket::http::Status;
use rocket::{Config, State}; use rocket::response::stream::{Event, EventStream};
use rocket::serde::json::Json;
use rocket::{Config, State, post};
use rocket::{get, http::ContentType, response::content::RawHtml, routes}; use rocket::{get, http::ContentType, response::content::RawHtml, routes};
use rust_embed::Embed; use rust_embed::Embed;
use serde::Deserialize;
use std::borrow::Cow; use std::borrow::Cow;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::sync::broadcast::Sender;
use crate::id_mapping::{IDMapping, Name};
use crate::id_store::IDStore; use crate::id_store::IDStore;
use crate::tally_id::TallyID;
#[derive(Embed)] #[derive(Embed)]
#[folder = "web/dist"] #[folder = "web/dist"]
struct Asset; struct Asset;
pub async fn start_webserver(store: Arc<Mutex<IDStore>>) -> Result<(), rocket::Error> { #[derive(Deserialize)]
struct NewMapping {
id: String,
name: Name,
}
pub async fn start_webserver(
store: Arc<Mutex<IDStore>>,
sse_broadcaster: Sender<String>,
) -> Result<(), rocket::Error> {
let port = match env::var("HTTP_PORT") { let port = match env::var("HTTP_PORT") {
Ok(port) => port.parse().unwrap_or_else(|_| { Ok(port) => port.parse().unwrap_or_else(|_| {
warn!("Failed to parse HTTP_PORT. Using default 80"); warn!("Failed to parse HTTP_PORT. Using default 80");
@ -31,8 +46,19 @@ pub async fn start_webserver(store: Arc<Mutex<IDStore>>) -> Result<(), rocket::E
}; };
rocket::custom(config) rocket::custom(config)
.mount("/", routes![static_files, index, export_csv]) .mount(
"/",
routes![
static_files,
index,
export_csv,
id_event,
get_mapping,
add_mapping
],
)
.manage(store) .manage(store)
.manage(sse_broadcaster)
.launch() .launch()
.await?; .await?;
Ok(()) Ok(())
@ -57,14 +83,50 @@ fn static_files(file: std::path::PathBuf) -> Option<(ContentType, Vec<u8>)> {
Some((content_type, asset.data.into_owned())) Some((content_type, asset.data.into_owned()))
} }
#[get("/api/idevent")]
fn id_event(sse_broadcaster: &State<Sender<String>>) -> EventStream![] {
let mut rx = sse_broadcaster.subscribe();
EventStream! {
loop {
let msg = rx.recv().await;
if let Ok(id) = msg {
yield Event::data(id);
}
}
}
}
#[get("/api/csv")] #[get("/api/csv")]
async fn export_csv(manager: &State<Arc<Mutex<IDStore>>>) -> Result<String, Status> { async fn export_csv(manager: &State<Arc<Mutex<IDStore>>>) -> Result<String, Status> {
info!("Exporting CSV"); info!("Exporting CSV");
match manager.lock().await.export_csv() { match manager.lock().await.export_csv() {
Ok(csv) => Ok(csv), Ok(csv) => Ok(csv),
Err(e) => { Err(e) => {
error!("Failed to generate csv: {}", e); error!("Failed to generate csv: {e}");
Err(Status::InternalServerError) Err(Status::InternalServerError)
} }
} }
} }
#[get("/api/mapping")]
async fn get_mapping(store: &State<Arc<Mutex<IDStore>>>) -> Json<IDMapping> {
Json(store.lock().await.mapping.clone())
}
#[post("/api/mapping", format = "json", data = "<new_mapping>")]
async fn add_mapping(store: &State<Arc<Mutex<IDStore>>>, new_mapping: Json<NewMapping>) -> Status {
if new_mapping.id.is_empty()
|| new_mapping.name.first.is_empty()
|| new_mapping.name.last.is_empty()
{
return Status::BadRequest;
}
store
.lock()
.await
.mapping
.add_mapping(TallyID(new_mapping.id.clone()), new_mapping.name.clone());
Status::Created
}

View File

@ -1,7 +1,6 @@
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" /> <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
@ -10,19 +9,9 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<title>Anwesenheit</title> <title>Anwesenheit</title>
</head> </head>
<body>
<body class="bg-gradient-to-br from-blue-100 to-indigo-200 min-h-screen flex flex-col items-center justify-start py-10"> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<div class="text-center space-y-6 mb-10"> </body>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">
Anwesenheit
</div>
<a class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
href="/api/csv" download="anwesenheit.csv">
Download CSV
</a>
</div>
</body>
</html> </html>

1144
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,15 +5,20 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
}, },
"devDependencies": { "devDependencies": {
"typescript": "~5.7.2", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"vite": "^6.2.0" "@tsconfig/svelte": "^5.0.4",
"svelte": "^5.28.1",
"svelte-check": "^4.1.6",
"typescript": "~5.8.3",
"vite": "^6.3.5"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.7",
"tailwindcss": "^4.1.4" "tailwindcss": "^4.1.7"
} }
} }

54
web/src/App.svelte Normal file
View File

@ -0,0 +1,54 @@
<script lang="ts">
import { onMount } from "svelte";
import IDTable from "./lib/IDTable.svelte";
import LastId from "./lib/LastID.svelte";
import AddIDModal from "./lib/AddIDModal.svelte";
let lastID: string = $state("");
let addModal: AddIDModal;
let idTable: IDTable;
onMount(() => {
let sse = new EventSource("/api/idevent");
sse.onmessage = (e) => {
lastID = e.data;
};
});
</script>
<main
class="bg-gradient-to-br from-blue-100 to-indigo-200 min-h-screen flex flex-col items-center justify-start py-10"
>
<div class="text-center space-y-6 mb-10">
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
</div>
<a
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
href="/api/csv"
download="anwesenheit.csv"
>
Download CSV
</a>
<div class="pt-3 pb-2">
<LastId
id={lastID}
onAdd={(id) => {
addModal.open(id);
}}
/>
</div>
<div>
<IDTable bind:this={idTable} />
</div>
<AddIDModal
bind:this={addModal}
onSubmitted={() => {
idTable.reloadData();
}}
/>
</main>

View File

@ -0,0 +1,90 @@
<script lang="ts">
import Modal from "./Modal.svelte";
let { onSubmitted }: { onSubmitted?: () => void } = $props();
let displayID = $state("");
let firstName = $state("");
let lastName = $state("");
let modal: Modal;
export function open(id: string) {
displayID = id;
modal.open();
}
function onsubmit() {
let data = {
id: displayID,
name: {
first: firstName,
last: lastName,
},
};
fetch("/api/mapping", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then(() => {
onSubmitted?.();
});
firstName = "";
lastName = "";
displayID = "";
}
</script>
<Modal bind:this={modal}>
<form method="dialog" {onsubmit} class="flex flex-col">
<label class="form-row">
<span>ID:</span>
<input type="text" class="form-input" required bind:value={displayID} />
</label>
<label class="form-row">
<span>Vorname:</span>
<input type="text" class="form-input" required bind:value={firstName} />
</label>
<label class="form-row">
<span>Nachname:</span>
<input type="text" class="form-input" required bind:value={lastName} />
</label>
<div class="flex justify-end mt-3">
<button
type="reset"
class="mr-5 px-2 py-1 bg-red-500 rounded-2xl shadow-md"
onclick={() => {
modal.close();
firstName = "";
lastName = "";
displayID = "";
}}>Abbrechen</button
>
<button
type="submit"
class="px-2 py-1 bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
>Hinzufügen</button
>
</div>
</form>
</Modal>
<style scoped>
@reference "../app.css";
.form-row {
@apply flex justify-between;
}
.form-input {
@apply ml-10 border-b-1;
}
</style>

32
web/src/lib/IDMapping.ts Normal file
View File

@ -0,0 +1,32 @@
export interface IDMapping {
id_map: IDMap
}
export interface IDMap {
[name: string]: Name
}
export interface Name {
first: string,
last: string,
}
export async function addMapping(id: string, firstName: string, lastName: string) {
let req = await fetch("/api/mapping", {
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8"
},
body: JSON.stringify({
id,
name: {
first: firstName,
lastName: lastName,
},
})
});
if (req.status != 200) {
console.error(await req.text())
}
}

106
web/src/lib/IDTable.svelte Normal file
View File

@ -0,0 +1,106 @@
<script lang="ts">
import { onMount } from "svelte";
import type { IDMapping } from "./IDMapping";
let data: IDMapping | undefined = $state();
export async function reloadData() {
let res = await fetch("/api/mapping");
data = await res.json();
}
let rows = $derived(
data
? Object.entries(data.id_map).map(([id, value]) => ({
id,
...value,
}))
: [],
);
let sortKey: keyof (typeof rows)[0] = $state("last");
let sortDirection: "asc" | "desc" = $state("asc");
let rowsSorted = $derived(
[...rows].sort((a, b) => {
let cmp = String(a[sortKey]).localeCompare(String(b[sortKey]));
return sortDirection === "asc" ? cmp : -cmp;
}),
);
function handleSortClick(key: keyof (typeof rows)[0]) {
if (sortKey === key) {
sortDirection = sortDirection === "asc" ? "desc" : "asc";
} else {
sortKey = key;
sortDirection = "asc";
}
}
function indicator(key: keyof (typeof rows)[0]) {
if (sortKey !== key) return "";
return sortDirection === "asc" ? "▲" : "▼";
}
onMount(async () => {
await reloadData();
});
</script>
{#if data == null}
Loading...
{:else}
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
<table class="px-10">
<thead>
<tr>
<th
class="text-left pr-5 pl-2 cursor-pointer select-none"
onclick={() => {
handleSortClick("id");
}}
>
ID
<span class="indicator">{indicator("id")}</span>
</th>
<th
class="text-left pr-5 cursor-pointer select-none"
onclick={() => {
handleSortClick("last");
}}
>
Nachname
<span class="indicator">{indicator("last")}</span>
</th>
<th
class="text-left pr-5 cursor-pointer select-none"
onclick={() => {
handleSortClick("first");
}}
>Vorname
<span class="indicator">{indicator("first")}</span>
</th>
</tr>
</thead>
<tbody>
{#each rowsSorted as row}
<tr class="even:bg-indigo-600">
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
<td class="whitespace-nowrap pr-5">{row.last}</td>
<td class="whitespace-nowrap pr-5">{row.first}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<style lang="css" scoped>
@reference "../app.css";
.indicator {
@apply ml-1 w-4 inline-block;
}
</style>

57
web/src/lib/LastID.svelte Normal file
View File

@ -0,0 +1,57 @@
<script lang="ts">
let { id, onAdd }: { id: string; onAdd?: (id: string) => void } = $props();
let lastID = id;
let flashing = $state(false);
$effect(() => {
if (lastID != id) {
flashing = true;
setTimeout(() => {
flashing = false;
}, 1100);
}
lastID = id;
});
</script>
<div class=" text-xl text-center">
Letzte ID
<div class="flex justify-center">
<span
class="{flashing
? 'flash'
: ''} font-bold rounded-md px-1 font-mono min-w-36">{id}</span
>
<button
class="bg-indigo-500 rounded-2xl px-2 cursor-pointer mx-2"
onclick={() => {
if (onAdd && id != "") {
onAdd(id);
}
}}>+</button
>
</div>
</div>
<style scoped>
.flash {
animation: flash-green 1s;
}
@keyframes flash-green {
0% {
background-color: transparent;
}
40% {
background-color: oklch(59.6% 0.145 163.225);
}
60% {
background-color: oklch(59.6% 0.145 163.225);
}
100% {
background-color: transparent;
}
}
</style>

33
web/src/lib/Modal.svelte Normal file
View File

@ -0,0 +1,33 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
let dialog: HTMLDialogElement;
export function open(){
dialog.showModal()
}
export function close(){
dialog.close();
}
</script>
<dialog
bind:this={dialog}
closedby="any"
class="bg-gradient-to-br from-blue-100 to-indigo-200 p-5 center-dialog rounded-2xl backdrop:bg-black/50 backdrop:backdrop-blur-xs"
>
{@render children?.()}
</dialog>
<style scoped>
.center-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -1,2 +1,9 @@
import './style.css' import { mount } from "svelte"
import "./app.css"
import App from "./App.svelte"
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app

View File

@ -1 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" /> /// <reference types="vite/client" />

7
web/svelte.config.js Normal file
View File

@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

20
web/tsconfig.app.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"moduleDetection": "force"
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

View File

@ -1,24 +1,7 @@
{ {
"compilerOptions": { "files": [],
"target": "ES2020", "references": [
"useDefineForClassFields": true, { "path": "./tsconfig.app.json" },
"module": "ESNext", { "path": "./tsconfig.node.json" }
"lib": ["ES2020", "DOM", "DOM.Iterable"], ]
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
} }

25
web/tsconfig.node.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,8 +1,18 @@
import { defineConfig } from "vite" import { defineConfig } from "vite"
import tailwindcss from "@tailwindcss/vite" import { svelte } from "@sveltejs/vite-plugin-svelte"
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
svelte()
], ],
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
},
},
},
}) })