chore: started working on the api

This commit is contained in:
Andre Henriques 2024-02-23 23:49:23 +00:00
parent 0fe7c51bab
commit 1c0d6a309b
33 changed files with 873 additions and 3 deletions

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect

2
go.sum
View File

@ -16,6 +16,8 @@ github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99 h1:8Bt1P/zy1gb37L4n8C
github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99/go.mod h1:3YgYBeIX42t83uP27Bd4bSMxTnQhSbxl0pYSkCDB1tc= github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99/go.mod h1:3YgYBeIX42t83uP27Bd4bSMxTnQhSbxl0pYSkCDB1tc=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=

View File

@ -14,6 +14,7 @@ import (
dbtypes "git.andr3h3nriqu3s.com/andr3/fyp/logic/db_types" dbtypes "git.andr3h3nriqu3s.com/andr3/fyp/logic/db_types"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/goccy/go-json"
) )
func Mul(n1 int, n2 int) int { func Mul(n1 int, n2 int) int {
@ -347,6 +348,43 @@ type Context struct {
Db *sql.DB Db *sql.DB
} }
func (c Context) ToJSON(r *http.Request, dat any) *Error {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(dat)
if err != nil {
return c.Error500(err)
}
return nil
}
func (c Context) SendJSON(w http.ResponseWriter, dat any) *Error {
w.Header().Add("content-type", "application/json")
text, err := json.Marshal(dat)
if err != nil {
return c.Error500(err)
}
if _, err = w.Write(text); err != nil {
return c.Error500(err)
}
return nil
}
func (c Context) SendJSONStatus(w http.ResponseWriter, status int, dat any) *Error {
w.Header().Add("content-type", "application/json")
w.WriteHeader(status)
text, err := json.Marshal(dat)
if err != nil {
return c.Error500(err)
}
if _, err = w.Write(text); err != nil {
return c.Error500(err)
}
return nil
}
func (c Context) Error400(err error, message string, w http.ResponseWriter, path string, base string, data AnyMap) *Error { func (c Context) Error400(err error, message string, w http.ResponseWriter, path string, base string, data AnyMap) *Error {
c.SetReportCaller(true) c.SetReportCaller(true)
c.Logger.Error(message) c.Logger.Error(message)
@ -609,6 +647,10 @@ func NewHandler(db *sql.DB) *Handle {
if r.Header.Get("Request-Type") == "htmlfull" { if r.Header.Get("Request-Type") == "htmlfull" {
ans = HTMLFULL ans = HTMLFULL
} }
if r.Header.Get("content-type") == "application/json" {
ans = JSON
}
//TODO JSON //TODO JSON
//Login state //Login state
@ -618,6 +660,9 @@ func NewHandler(db *sql.DB) *Handle {
return return
} }
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
if r.Method == "GET" { if r.Method == "GET" {
x.handleGets(w, r, context) x.handleGets(w, r, context)
return return
@ -630,6 +675,9 @@ func NewHandler(db *sql.DB) *Handle {
x.handleDeletes(w, r, context) x.handleDeletes(w, r, context)
return return
} }
if r.Method == "OPTIONS" {
return
}
panic("TODO handle method: " + r.Method) panic("TODO handle method: " + r.Method)
}) })

View File

@ -4,7 +4,6 @@ import (
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"fmt"
"io" "io"
"net/http" "net/http"
"time" "time"
@ -79,8 +78,39 @@ func usersEndpints(db *sql.DB, handle *Handle) {
handle.GetHTML("/login", AnswerTemplate("login.html", nil, 0)) handle.GetHTML("/login", AnswerTemplate("login.html", nil, 0))
handle.Post("/login", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { handle.Post("/login", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
if c.Mode == JSON { if c.Mode == JSON {
fmt.Println("Handle JSON")
return &Error{Code: 404} type UserLogin struct {
Email string `json:email`
Password string `json:password`
}
var dat UserLogin
if err := c.ToJSON(r, &dat); err != nil {
return err
}
/*if (dat["email"] == nil || dat["password"] == nil) {
// TODO improve this
c.Logger.Warn("Email or password are empty")
return c.Error500(nil)
}*/
// TODO Give this to the generateToken function
expiration := time.Now().Add(24 * time.Hour)
token, login := generateToken(db, dat.Email, dat.Password)
if !login {
return c.SendJSONStatus(w, http.StatusUnauthorized, "Email or password are incorrect")
}
cookie := &http.Cookie{Name: "auth", Value: token, HttpOnly: false, Expires: expiration}
http.SetCookie(w, cookie)
w.Header().Set("Location", "/")
w.WriteHeader(http.StatusSeeOther)
return nil
} }
r.ParseForm() r.ParseForm()

13
webpage/.eslintignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

31
webpage/.eslintrc.cjs Normal file
View File

@ -0,0 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

10
webpage/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
webpage/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
webpage/.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

8
webpage/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

38
webpage/README.md Normal file
View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/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.

BIN
webpage/bun.lockb Executable file

Binary file not shown.

34
webpage/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "webpage",
"version": "0.0.1",
"private": true,
"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 --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0-next.4",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"sass": "^1.71.1",
"svelte": "^5.0.0-next.1",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module"
}

60
webpage/src/NavBar.svelte Normal file
View File

@ -0,0 +1,60 @@
<script lang="ts">
import { userStore } from './routes/UserStore.svelte';
</script>
<nav>
<ul>
<li>
<a href="/"> Index </a>
</li>
{#if userStore.user}
<li>
<a href="/models"> Models </a>
</li>
{/if}
<li class="expand"></li>
{#if userStore.user}
<li>
<a href="/user/info"> User Info </a>
<a href="/logout"> Logout </a>
</li>
{:else}
<li>
<a href="/login"> Login </a>
</li>
{/if}
</ul>
</nav>
<style class="scss">
nav {
background: #ececec;
margin: 0;
box-shadow: 0 0 8px 1px #888888ef;
height: 60px;
ul {
display: flex;
margin: 0;
padding: 20px 40px;
li {
list-style: none;
padding-left: 10px;
&:first-child {
padding: 0;
}
&.expand {
flex-grow: 1;
}
a {
text-decoration: none;
color: black;
}
}
}
}
</style>

13
webpage/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
webpage/src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

1
webpage/src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -0,0 +1,42 @@
import { userStore } from 'routes/UserStore.svelte';
const API = "http://localhost:8000";
export async function get(url: string) {
const headers = new Headers();
//headers.append('content-type', 'application/json');
if (userStore.user) {
headers.append('token', userStore.user.token);
}
let r = await fetch(`${API}/${url}`, {
method: 'GET',
headers: headers,
});
if (r.status !== 200) {
throw r;
}
return r.json();
}
export async function post(url: string, body: any) {
const headers = new Headers();
headers.append('content-type', 'application/json');
if (userStore.user) {
headers.append('token', userStore.user.token);
}
let r = await fetch(`${API}/${url}`, {
method: 'POST',
headers: headers,
body: JSON.stringify(body),
});
if (r.status !== 200) {
throw r;
}
return r.json();
}

View File

@ -0,0 +1,17 @@
<script lang="ts">
import { page } from '$app/stores';
import Page404 from './Page404.svelte';
</script>
{#if $page.error}
{#if $page.status == 404}
<Page404 />
{:else}
<h1>
{$page.status}
</h1>
<h2>
{$page.error.message}
</h2>
{/if}
{/if}

View File

@ -0,0 +1,9 @@
<script lang="ts">
import '../styles/fonts.css';
import '../styles/app.css';
import NavBar from "../NavBar.svelte";
</script>
<NavBar />
<slot />

View File

@ -0,0 +1,4 @@
export const prerender = true;
export const ssr = false;
export const csr = true;
//export const trailingSlash = '';

View File

@ -0,0 +1 @@
<h1>Main Web Page</h1>

View File

@ -0,0 +1,54 @@
<script lang="ts">
let { message, goBackLink } = $props<{message?: string, goBackLink?: string}>();
</script>
<div class="page404">
<div>
<h1>
404
</h1>
{#if message}
<h2>
{message}
</h2>
{#if goBackLink}
<div class="description">
<a href={goBackLink}>
👈 Go back
</a>
</div>
{/if}
{:else}
<h2>
Page Not found
</h2>
<div class="description">
The page you were looking for does not exist
</div>
{/if}
</div>
</div>
<style lang="scss">
.page404 {
display: grid;
place-items: center;
height: calc(100vh - 60px);
text-align: center;
h1 {
font-size: 10em;
margin: 0;
}
h2 {
font-size: 5em;
margin: 0;
margin-bottom: 0.3em;
}
div.description {
font-size: 1.5em;
}
}
</style>

View File

@ -0,0 +1,55 @@
import { goto } from "$app/navigation";
type User = {
token: string,
id: string,
user_type: number,
username: string,
email: string,
};
export function createUserStore() {
let user = $state<User | undefined>(undefined);
function getValue() {
if (user == undefined) {
let storage = localStorage.getItem('user');
if (storage) {
try {
user = JSON.parse(storage);
} catch {
user = undefined;
}
}
}
return user;
}
return {
get user() {
return getValue();
},
set user(value: User | undefined) {
if (value) {
localStorage.setItem('user', JSON.stringify(value));
} else {
localStorage.removeItem('user');
}
user = value;
},
checkUser(pathOnFail: string, level?: number) {
if (user && user.level > (level ?? 2) ) {
goto(pathOnFail);
return true;
}
return false;
},
isLogin() {
if (getValue()) return true;
return false;
}
}
}
export const userStore = createUserStore();

View File

@ -0,0 +1,83 @@
<script lang="ts">
import { post } from 'src/lib/requests.svelte';
import 'src/styles/forms.css';
import { userStore } from '../UserStore.svelte';
import { goto } from '$app/navigation';
let submitted = $state(false);
let loginData = $state({
email: '',
password: '',
});
let errorMessage = $state("");
async function onSubmit() {
submitted = true;
errorMessage = "";
try {
const req = await post('login', loginData);
userStore.user = req;
goto("/");
} catch (e) {
if (e instanceof Response) {
errorMessage = await e.json();
} else {
errorMessage = "Server could not perform login";
}
}
}
</script>
<svelte:head>
<title> Login </title>
</svelte:head>
<div class="login-page">
<div>
<h1>
Login
</h1>
<form on:submit|preventDefault={onSubmit} class:submitted >
<fieldset>
<label for="email">Email</label>
<input type="email" required name="email" bind:value={loginData.email} />
</fieldset>
<fieldset>
<label for="password">Password</label>
<input required name="password" type="password" bind:value={loginData.password} />
{#if errorMessage}
<span class="form-msg error">
{errorMessage}
<!--Either the password or the email are incorrect-->
</span>
{/if}
</fieldset>
<button>
Login
</button>
<div class="spacer"></div>
<a class="simple-link text-center w100 spacer" href="/register">
Register
</a>
</form>
</div>
</div>
<style lang="scss">
/* Login Page */
.login-page {
display: grid;
place-items: center;
margin-bottom: 40px;
& > div {
width: 40vw;
}
h1 {
text-align: center;
}
}
</style>

View File

@ -0,0 +1,10 @@
<script lang="ts">
import { onMount } from "svelte";
import { userStore } from "../UserStore.svelte";
import { goto } from "$app/navigation";
onMount(() => {
userStore.user = undefined;
goto('/');
});
</script>

View File

@ -0,0 +1,88 @@
*{box-sizing: border-box;font-family: 'Roboto', sans-serif;}
:root {
--nav-bar-size: 54px;
--white: #ffffff;
--grey: #dbdcde;
--light-grey: #fafafa;
--main: #fca311;
--sec: #14213d;
--black: #000000;
--red: 212, 38, 38;
--green: 92, 199, 30;
}
body {
margin: 0px;
}
.button,
button {
border-radius: 10px;
text-align: center;
padding: 3px 6px;
border: none;
box-shadow: 0 2px 8px 1px #66666655;
background: var(--main);
color: var(--black);
&.padded {
padding: 10px;
}
&.danger {
background: rgb(var(--red));
color: white;
font-weight: bold;
}
}
.flex {
display: flex;
}
.justify-center {
justify-content: center;
}
.justify-start {
justify-content: start;
}
.justify-end {
justify-content: end;
}
.align-center {
align-items: center;
}
.grow-1 {
flex-grow: 1;
}
.w100 {
width: 100%;
display: block;
}
.text-center {
text-align: center;
}
.simple-link {
color: var(--sec);
text-decoration: none;
}
.bold {
font-weight: bold;
}
.bigger {
font-size: 1.1rem;
}
.danger {
color: red;
}

View File

@ -0,0 +1,8 @@
@font-face {
font-family: MedievalSharp;
src: url(/MedievalSharp-Regular.ttf);
}
.font-medieval {
font-family: MedievalSharp, cursive;
}

View File

@ -0,0 +1,147 @@
/* forms */
a {
cursor: pointer;
}
.card form {
padding: 0;
border-radius: none;
box-shadow: none;
}
form {
padding: 30px;
margin: 20px 0;
border-radius: 10px;
box-shadow: 2px 5px 8px 2px #66666655;
label,
fieldset legend {
display: block;
padding-bottom: 5px;
font-size: 1.2rem;
}
input {
border: none;
box-shadow: 0 2px 5px 1px #66666655;
border-radius: 5px;
padding: 10px;
width: 100%;
}
input:invalid:focus,
&.submitted input:invalid {
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
}
&.submitted input:valid {
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
}
& .spacer {
padding-bottom: 10px;
}
fieldset {
padding-bottom: 15px;
border: none;
.form-msg {
font-size: 0.9rem;
}
.error {
color: rgb(var(--red))
}
}
button {
font-size: 1.2rem;
margin-left: 50%;
width: 50%;
transform: translateX(-50%);
padding: 10px;
}
.input-radial {
label {
display: inline;
font-size: 1rem;
}
input[type="radio"] {
width: auto;
box-shadow: none;
}
}
/* Upload files */
fieldset.file-upload {
input[type="file"] {
height: 1px;
width: 1px;
padding: 0;
box-shadow: none;
}
.icon-holder {
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
font-size: 1rem;
.icon {
height: 150px;
width: 150px;
padding: 20px;
border-radius: 10px;
background: none;
transform: none;
margin: 0;
font-size: 1rem;
transition: all 1s;
&.adapt {
width: auto;
height: auto;
max-width: 80%;
max-height: 80%;
min-height: 150px;
min-width: 150px;
padding: 20px;
}
img {
display: block;
width: 100%;
height: 80%;
object-fit: contain;
text-align: center;
transition: all 1s;
}
span {
display: block;
width: 100%;
padding-top: 10px;
text-align: center;
}
}
}
}
fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon,
&.submitted fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon {
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
}
&.submitted fieldset.file-upload:has(input[type="file"]:valid:focus) .icon {
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
}
}

BIN
webpage/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

22
webpage/svelte.config.js Normal file
View File

@ -0,0 +1,22 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
alias: {
src: "src",
routes: "src/routes",
}
}
};
export default config;

18
webpage/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// 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
}

6
webpage/vite.config.ts Normal file
View File

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});