diff options
| author | gammelalf <gammelalf@hopfen.space> | 2023-01-23 03:12:21 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-02-24 12:47:40 +0100 |
| commit | 7b9d908d2c1612843990dd39decbdbf40e51dbd7 (patch) | |
| tree | 540f2c6aaf25f9251e4666dc56f0afbf85b2944c | |
| parent | 95f95783383a58af5250bcb66155a04971385b75 (diff) | |
| download | WRMS-7b9d908d2c1612843990dd39decbdbf40e51dbd7.tar.gz WRMS-7b9d908d2c1612843990dd39decbdbf40e51dbd7.zip | |
Reimplemented current frontend in svelte
| -rw-r--r-- | main.go | 15 | ||||
| -rw-r--r-- | web/.eslintrc.cjs | 20 | ||||
| -rw-r--r-- | web/.gitignore | 6 | ||||
| -rw-r--r-- | web/.prettierignore | 3 | ||||
| -rw-r--r-- | web/README.md | 38 | ||||
| -rw-r--r-- | web/client.html | 294 | ||||
| -rw-r--r-- | web/package.json | 59 | ||||
| -rw-r--r-- | web/src/app.d.ts | 12 | ||||
| -rw-r--r-- | web/src/app.html | 13 | ||||
| -rw-r--r-- | web/src/lib/api.ts | 40 | ||||
| -rw-r--r-- | web/src/routes/+page.svelte | 159 | ||||
| -rw-r--r-- | web/src/routes/+page.ts | 1 | ||||
| -rw-r--r-- | web/static/client.css | 67 | ||||
| -rw-r--r-- | web/svelte.config.js | 23 | ||||
| -rw-r--r-- | web/tsconfig.json | 17 | ||||
| -rw-r--r-- | web/vite.config.ts | 8 |
16 files changed, 427 insertions, 348 deletions
@@ -5,7 +5,6 @@ import ( "encoding/json" "flag" "fmt" - "html/template" "io/ioutil" "net/http" @@ -19,17 +18,7 @@ import ( var wrms *Wrms func landingPage(w http.ResponseWriter, r *http.Request) { - if _, err := r.Cookie("UUID"); err != nil { - http.SetCookie(w, &http.Cookie{Name: "UUID", Value: uuid.NewString()}) - } - - t, err := template.ParseFiles("web/client.html") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - t.Execute(w, "") + http.Redirect(w, r, "/static/index.html", 301) } func searchHandler(w http.ResponseWriter, r *http.Request) { @@ -209,7 +198,7 @@ func setupRoutes() { http.HandleFunc("/add", addHandler) http.HandleFunc("/playpause", playPauseHandler) http.HandleFunc("/ws", wsEndpoint) - http.Handle("/static/", http.FileServer(http.Dir("./web"))) + http.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir("./web/build")))) } func main() { diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 0000000..3ccf435 --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + plugins: ['svelte3', '@typescript-eslint'], + ignorePatterns: ['*.cjs'], + overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], + settings: { + 'svelte3/typescript': () => require('typescript') + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020 + }, + env: { + browser: true, + es2017: true, + node: true + } +}; diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..c97ad87 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +build/ +.svelte-kit/ +.idea/ + +package-lock.json diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 0000000..29e170b --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,3 @@ +build/ +node_modules/ +.svelte-kit/ diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..5c91169 --- /dev/null +++ b/web/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/web/client.html b/web/client.html deleted file mode 100644 index 2fbc4fd..0000000 --- a/web/client.html +++ /dev/null @@ -1,294 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>WRMS</title> - - <script> - /* - @licstart The following is the entire license notice for the - JavaScript code in this page. - - Copyright (C) 2022 Florian Fischer - - The JavaScript code in this page is free software: you can - redistribute it and/or modify it under the terms of the GNU - General Public License (GNU GPL) as published by the Free Software - Foundation, either version 3 of the License, or (at your option) - any later version. The code is distributed WITHOUT ANY WARRANTY; - without even the implied warranty of MERCHANTABILITY or FITNESS - FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. - - As additional permission under GNU GPL version 3 section 7, you - may distribute non-source (e.g., minimized or compacted) forms of - that code without the copy of the GNU GPL normally required by - section 4, provided you include this license notice and a URL - through which recipients can access the Corresponding Source. - - - @licend The above is the entire license notice - for the JavaScript code in this page. - */ - </script> - - <script> - let PLAY_PAUSE; - - const HttpClient = function() { - function newRequest(callback) { - const request = new XMLHttpRequest(); - request.onreadystatechange = function() { - if (request.readyState === 4 && request.status === 200) - callback(request.responseText); - } - return request; - } - - this.get = function(url, callback) { - const request = newRequest(callback); - request.open("GET", url, true); - request.send(null); - } - - this.post = function(url, data, callback) { - const request = newRequest(callback); - request.open("POST", url, true); - request.send(data); - } - } - - let songs = []; - let playing = {}; - let votes = new Map(); - - function formatSong(song) { - if (song.artist) - return song.artist + ' - ' + song.title - return song.title - } - - function renderPlaylist() { - const playlist = document.getElementById("playlist"); - playlist.innerHTML = ""; - - songs.sort(function(a, b) {return b.weight - a.weight}); - - for (const song of songs) { - const listItem = document.createElement("li"); - - const btnHTML = "<svg width='36' height='36'><path d='M2 10h32L18 26 2 10z' fill='currentColor'></path></svg>"; - const upvoteBtn = document.createElement("span"); - const downvoteBtn = document.createElement("span"); - - upvoteBtn.innerHTML = downvoteBtn.innerHTML = btnHTML; - upvoteBtn.className = downvoteBtn.className = "vote"; - upvoteBtn.style.transform = 'rotate(180deg)'; - - if (votes.has(song.uri)) { - if (votes.get(song.uri) === "up") - upvoteBtn.classList.toggle("on"); - else - downvoteBtn.classList.toggle("on"); - } - - function onVote(event) { - const btn = event.currentTarget; - btn.classList.toggle("on"); - const isUpvoteBtn = btn === upvoteBtn; - const isVote = btn.classList.contains("on"); - - let url; - if (isVote) { - if (isUpvoteBtn) { - downvoteBtn.classList.remove("on") - votes.set(song.uri, "up") - } else { - upvoteBtn.classList.remove("on") - votes.set(song.uri, "down") - } - - url = "/" + (isUpvoteBtn ? "up" : "down") + "?song=" + encodeURIComponent(song.uri); - } else { - votes.delete(song.uri); - url = "/unvote?song=" + encodeURIComponent(song.uri); - } - - console.log("Get " + url); - new HttpClient().get(url, console.log); - } - upvoteBtn.addEventListener("click", onVote); - downvoteBtn.addEventListener("click", onVote); - - listItem.appendChild(upvoteBtn); - listItem.appendChild(downvoteBtn); - - listItem.appendChild(document.createTextNode(song.weight + ' ' + formatSong(song))); - const sourceLabel = document.createElement("SMALL"); - sourceLabel.innerHTML = " (" + song.source + ")"; - listItem.appendChild(sourceLabel); - playlist.appendChild(listItem); - } - } - - function handleAdd(added) { - songs = songs.concat(added); - renderPlaylist(); - } - - function handleVotes(direction, votedSongs) { - for (const song of votedSongs) { - votes.set(song.uri, direction) - } - renderPlaylist(); - } - - function handleUpdate(updated) { - for (const updated_song of updated) { - songs[songs.findIndex(function(s) {return s.uri === updated_song.uri})] = updated_song; - } - renderPlaylist(); - } - - function handlePause() { - PLAY_PAUSE.classList.remove("pause"); - PLAY_PAUSE.classList.add("play"); - } - - function handleStop() { - PLAY_PAUSE.classList.remove("pause"); - PLAY_PAUSE.classList.add("play"); - playing = document.getElementById("playing").innerHTML = ""; - } - - function handlePlay(currentSong) { - PLAY_PAUSE.classList.remove("play"); - PLAY_PAUSE.classList.add("pause"); - - let idx = -1; - songs.forEach(function(song, index) { - if (song.uri === currentSong.uri) - idx = index; - }); - if (idx !== -1) { - songs.splice(idx, 1); - renderPlaylist(); - } - - const songLabel = document.createTextNode(formatSong(currentSong)); - - playing = document.getElementById("playing"); - playing.innerHTML = ""; - playing.appendChild(songLabel); - } - - const socket = new WebSocket("ws://localhost:8080/ws"); - console.log("Attempting Connection..."); - - socket.onopen = () => { - console.log("Successfully Connected"); - }; - - socket.onclose = event => { - console.log("Socket Closed Connection: ", event); - }; - - socket.onmessage = message => { - console.log("Socket Received Message: ", message); - const cmd = JSON.parse(message.data); - switch (cmd.cmd) { - case "add": - handleAdd(cmd.songs) - break; - case "update": - handleUpdate(cmd.songs) - break; - case "pause": - handlePause() - break; - case "play": - handlePlay(cmd.songs[0]) - break; - case "upvoted": - handleVotes("up", cmd.songs) - break; - case "downvoted": - handleVotes("down", cmd.songs) - break; - } - }; - - socket.onerror = error => { - console.log("Socket Error: ", error); - }; - - function submitSearch() { - const form = new FormData(document.getElementById("searchForm")); - const pattern = new URLSearchParams(form).toString(); - new HttpClient().get("/search?" + pattern, showSearchResults); - return false; - } - - function addSong(song) { - new HttpClient().post("/add", JSON.stringify(song), console.log); - } - - function showSearchResults(responseText) { - const response = JSON.parse(responseText) - if (!response) - return - - const searchResults = document.getElementById("searchResults"); - - // Clear search Results - searchResults.innerHTML = ""; - - for (const song of response.songs) { - const listItem = document.createElement("div"); - listItem.className = "flex-row"; - - listItem.innerHTML = "<button class='icon-button'><img src='static/add.svg' alt='Add'></button>" + formatSong(song) + " <small>(" + song.source + ")</small>"; - listItem.querySelector("button").addEventListener("click", () => addSong(song)); - - searchResults.appendChild(listItem); - } - } - - window.onload = function() { - PLAY_PAUSE = document.getElementById("play-pause"); - PLAY_PAUSE.addEventListener("click", function() { - new HttpClient().get("/playpause", console.log); - }); - } - </script> - - <link rel="stylesheet" href="static/client.css"> - </head> - - <body> - <div class="flex-column"> - <h2>Currently Playing</h2> - <p id='playing'>Nothing</p> - <div id='controls'> - <button id="play-pause" class="icon-button play"> - <img src="static/play.svg" alt="Play"> - <img src="static/pause.svg" alt="Pause"> - </button> - </div> - - <h2>Add your song</h2> - <div id="add"> - <form id="searchForm" onsubmit="return submitSearch()"> - <label><input id="searchInput" name="pattern" type="text" placeholder="Title/Artist/Album/..."></label> - <button id="searchButton">Search</button> - </form> - </div> - - <div id="searchResults" class="flex-column"> - </div> - - <h2>Playlist</h2> - <ul id='playlist'></ul> - </div> - </body> -</html> diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..e6e0eef --- /dev/null +++ b/web/package.json @@ -0,0 +1,59 @@ +{ + "name": "wrms", + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^1.0.0", + "@sveltejs/adapter-static": "^1.0.5", + "@sveltejs/kit": "^1.0.0", + "@sveltejs/package": "^1.0.0", + "@tsconfig/svelte": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^5.45.0", + "@typescript-eslint/parser": "^5.45.0", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-svelte3": "^4.0.0", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.8.1", + "svelte": "^3.54.0", + "svelte-check": "^3.0.2", + "svelte-preprocess": "^5.0.1", + "tslib": "^2.4.1", + "typescript": "^4.9.4", + "vite": "^4.0.0" + }, + "prettier": { + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": [ + "prettier-plugin-svelte" + ], + "pluginSearchDirs": [ + "." + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] + }, + "eslintIgnore": [ + ".svelte-kit", + "build", + "node_modules" + ], + "type": "module" +} diff --git a/web/src/app.d.ts b/web/src/app.d.ts new file mode 100644 index 0000000..f59b884 --- /dev/null +++ b/web/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/web/src/app.html b/web/src/app.html new file mode 100644 index 0000000..1d67ac6 --- /dev/null +++ b/web/src/app.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WRMS</title> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="stylesheet" href="%sveltekit.assets%/client.css" /> + %sveltekit.head% + </head> + <body> + <div>%sveltekit.body%</div> + </body> +</html> diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..032cbd4 --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,40 @@ +export type Song = { + title: string; + artist: string; + source: string; + uri: string; + weight: number; +}; + +export type WsMessage = { + cmd: 'add' | 'update' | 'pause' | 'play' | 'upvoted' | 'downvoted'; + songs: Array<Song>; +}; +export type SearchResponse = { + cmd: 'search'; + songs: Array<Song>; +}; + +const HEADERS = new Headers([ + ['Accept', 'application/json'], + ['Content-Type', 'application/json'] +]); +const API = { + baseUrl: '', + get(url: string, body?: Record<string, string>) { + if (body !== undefined) { + url += '?' + new URLSearchParams(body); + } + return fetch(this.baseUrl + url, { + method: 'GET' + }); + }, + post(url: string, body?: object) { + return fetch(this.baseUrl + url, { + method: 'POST', + body: body && JSON.stringify(body), + headers: HEADERS + }); + } +}; +export default API; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte new file mode 100644 index 0000000..1cb4c71 --- /dev/null +++ b/web/src/routes/+page.svelte @@ -0,0 +1,159 @@ +<script lang="ts"> + import { onDestroy, onMount } from 'svelte'; + import addSVG from '../../static/play.svg'; + import playSVG from '../../static/play.svg'; + import pauseSVG from '../../static/pause.svg'; + import type { WsMessage, Song, SearchResponse } from '../lib/api'; + import API from '../lib/api'; + API.baseUrl = '/'; + + let status: 'paused' | 'playing' = 'paused'; + let currentSong: Song | null = null; + let playlist: Array<Song> = []; + let votes: Map<string, 'up' | 'down'> = new Map(); + + let search = ''; + let searchResults: Array<Song> = []; + + let socket: WebSocket; + onMount(function () { + socket = new WebSocket('ws://localhost:8080/ws'); + console.info('Attempting Connection...'); + + socket.onopen = () => { + console.info('Successfully Connected'); + }; + + socket.onclose = (event) => { + console.info('Socket Closed Connection: ', event); + }; + + socket.onerror = (error) => { + console.error('Socket Error: ', error); + }; + + socket.onmessage = ({ data }) => { + const { cmd, songs }: WsMessage = JSON.parse(data); + console.debug('Ws Message', cmd, songs); + switch (cmd) { + case 'add': + playlist = [...playlist, ...songs]; + break; + case 'update': + for (const song of songs) { + playlist[ + playlist.findIndex(({ uri }) => { + return uri === song.uri; + }) + ] = song; + } + break; + case 'pause': + status = 'paused'; + break; + case 'play': + status = 'playing'; + currentSong = songs[0]; + playlist = playlist.filter(({ uri }) => uri !== currentSong?.uri); + break; + case 'upvoted': + for (const { uri } of songs) { + votes.set(uri, 'up'); + } + votes = votes; + break; + case 'downvoted': + for (const { uri } of songs) { + votes.set(uri, 'down'); + } + votes = votes; + break; + } + }; + }); + onDestroy(function () { + socket?.close(); + }); + + $: playlist = playlist.sort(function (a, b) { + return b.weight - a.weight; + }); + + async function submitSearch() { + const response = await API.get('search', { pattern: search }); + const { songs }: SearchResponse = await response.json(); + searchResults = songs; + } + + function vote(direction: 'up' | 'down', uri: string) { + return async function () { + let action; + if (votes.get(uri) === direction) { + votes.delete(uri); + action = 'unvote'; + } else { + votes.set(uri, direction); + action = direction; + } + const response = await API.get(action, { song: uri }); + console.debug(response); + }; + } +</script> + +<div class="flex-column"> + <h2>Currently Playing</h2> + <p>{currentSong?.title || 'Nothing'}</p> + <div id="controls"> + <button class="icon-button play" on:click={() => API.get('playpause').then(console.debug)}> + {#if status === 'playing'} + <img src={pauseSVG} alt="Pause" /> + {:else} + <img src={playSVG} alt="Play" /> + {/if} + </button> + </div> + <div class="flex-row"> + <div class="flex-column"> + <h1>Add your song</h1> + <form on:submit|preventDefault={submitSearch}> + <label><input bind:value={search} placeholder="Title/Artist/Album/..." /></label> + <button formaction="submit">Search</button> + {#each searchResults as { title, artist, source, uri, weight } (uri)} + <div class="flex-row search-results"> + <button + class="icon-button" + on:click={() => + API.post('add', { title, artist, source, uri, weight }).then(console.debug)} + > + <img src={addSVG} alt="Add" /> + </button> + {artist + ' - ' + title} + <small>{source}</small> + </div> + {/each} + </form> + </div> + <div class="flex-column"> + <h1>Playlist</h1> + {#each playlist as { title, artist, source, uri, weight } (uri)} + <div> + <button class="vote" on:click={vote('up', uri)}>Up</button> + <button class="vote" on:click={vote('down', uri)}>Down</button> + {`${weight} ${artist} - ${title}`} + <small>{`(${source})`}</small> + </div> + {/each} + </div> + </div> +</div> + +<style> + .search-results > button { + --icon-size: 2em; + } + .search-results > small { + color: var(--secondary); + margin-left: 0.5em; + } +</style> diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/web/src/routes/+page.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/web/static/client.css b/web/static/client.css index c01b307..ba4c864 100644 --- a/web/static/client.css +++ b/web/static/client.css @@ -1,60 +1,45 @@ :root { - --background: #222222; - --primary: white; - --secondary: gray; - --link: yellow; + --background: #222222; + --primary: white; + --secondary: gray; + --link: yellow; } html { - width: 100%; - overflow: hidden; + width: 100%; + overflow: hidden; } body { - position: fixed; - overflow-y: scroll; - -webkit-overflow-scrolling: touch; - width: 100%; - height: 100%; - margin: 0; - background-color: var(--background); - color: var(--primary); + position: fixed; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + width: 100%; + height: 100%; + margin: 0; + background-color: var(--background); + color: var(--primary); } .flex-column { - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; } .flex-row { - display: flex; - flex-direction: row; - align-items: center; + display: flex; + flex-direction: row; + align-items: center; } .icon-button { - --icon-size: 4em; - border: none; - background-color: transparent; + --icon-size: 4em; + border: none; + background-color: transparent; } .icon-button > img { - width: var(--icon-size); - height: var(--icon-size); - filter: invert(100%); -} - -#play-pause.pause > img:nth-child(1) { - display: none; -} -#play-pause.play > img:nth-child(2) { - display: none; -} - -#searchResults > div > button { - --icon-size: 2em; -} -#searchResults > div > small { - color: var(--secondary); - margin-left: 0.5em; + width: var(--icon-size); + height: var(--icon-size); + filter: invert(100%); } diff --git a/web/svelte.config.js b/web/svelte.config.js new file mode 100644 index 0000000..8c8f77b --- /dev/null +++ b/web/svelte.config.js @@ -0,0 +1,23 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/kit/vite'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + adapter: adapter({ + // default options are shown. On some platforms + // these options are set automatically — see below + pages: 'build', + assets: 'build', + fallback: null, + precompress: false, + strict: true + }) + } +}; + +export default config; diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..6ae0c8c --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..1695034 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,8 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import type { UserConfig } from 'vite'; + +const config: UserConfig = { + plugins: [sveltekit()] +}; + +export default config; |
