mirror of
https://github.com/sofiaritz/aybWeb
synced 2023-12-10 14:23:29 +00:00
aybWeb (again)
This commit is contained in:
commit
ebd9f7ae36
35 changed files with 2247 additions and 0 deletions
7
.editorconfig
Normal file
7
.editorconfig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
47
README.md
Normal file
47
README.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Svelte + TS + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Svelte and TypeScript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||||
|
|
||||||
|
## Need an official Svelte framework?
|
||||||
|
|
||||||
|
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||||
|
|
||||||
|
## Technical considerations
|
||||||
|
|
||||||
|
**Why use this over SvelteKit?**
|
||||||
|
|
||||||
|
- It brings its own routing solution which might not be preferable for some users.
|
||||||
|
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||||
|
|
||||||
|
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||||
|
|
||||||
|
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||||
|
|
||||||
|
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||||
|
|
||||||
|
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
||||||
|
|
||||||
|
**Why include `.vscode/extensions.json`?**
|
||||||
|
|
||||||
|
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||||
|
|
||||||
|
**Why enable `allowJs` in the TS template?**
|
||||||
|
|
||||||
|
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
|
||||||
|
|
||||||
|
**Why is HMR not preserving my local component state?**
|
||||||
|
|
||||||
|
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||||
|
|
||||||
|
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// store.ts
|
||||||
|
// An extremely simple external store
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
export default writable(0)
|
||||||
|
```
|
12
index.html
Normal file
12
index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>aybWeb</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
package.json
Normal file
33
package.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "ayb-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"prettify": "yarn prettier . --write"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@tsconfig/svelte": "^5.0.2",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"felte": "^1.2.12",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"prettier": "^3.1.0",
|
||||||
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||||
|
"svelte": "^4.2.3",
|
||||||
|
"svelte-check": "^3.6.0",
|
||||||
|
"svelte-routing": "sofiaritz/svelte-routing-exports",
|
||||||
|
"tailwindcss": "^3.3.6",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pattern.css": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
0
public/.gitkeep
Normal file
0
public/.gitkeep
Normal file
40
src/App.svelte
Normal file
40
src/App.svelte
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Header from "./lib/components/Header.svelte"
|
||||||
|
import { Router, Route } from "svelte-routing"
|
||||||
|
import { loggedIn } from "./lib/auth/stores"
|
||||||
|
import Redirect from "./lib/components/Redirect.svelte"
|
||||||
|
import Login from "./routes/Login.svelte"
|
||||||
|
import Confirm from "./routes/Confirm.svelte"
|
||||||
|
import Home from "./routes/Home.svelte";
|
||||||
|
import DatabaseOverview from "./routes/DatabaseOverview.svelte";
|
||||||
|
import DatabaseQuery from "./routes/DatabaseQuery.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Router>
|
||||||
|
<Header />
|
||||||
|
<div class="mt-8 flex min-h-full flex-col items-center">
|
||||||
|
<main class="flex w-[95vw] flex-col items-center md:w-[60vw]">
|
||||||
|
{#if $loggedIn}
|
||||||
|
<Route path="/"><Home/></Route>
|
||||||
|
<Route path="/database/:entity/:slug/overview" let:params>
|
||||||
|
<DatabaseOverview entity={params.entity} slug={params.slug} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/database/:entity/:slug/query" let:params>
|
||||||
|
<DatabaseQuery entity={params.entity} slug={params.slug} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/auth/login"><Redirect to="/"/></Route>
|
||||||
|
<Route path="/auth/confirm"><Redirect to="/"/></Route>
|
||||||
|
<Route path="/auth/confirm/:token">
|
||||||
|
<Redirect to="/"/>
|
||||||
|
</Route>
|
||||||
|
{:else}
|
||||||
|
<Route path="/"><Redirect to="/auth/login" /></Route>
|
||||||
|
<Route path="/auth/login"><Login /></Route>
|
||||||
|
<Route path="/auth/confirm"><Confirm token={undefined} /></Route>
|
||||||
|
<Route path="/auth/confirm/:token" let:params>
|
||||||
|
<Confirm token={params.token} />
|
||||||
|
</Route>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Router>
|
37
src/app.css
Normal file
37
src/app.css
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("https://cdn.sofiaritz.com/fonts/ofl/jetbrainsmono/JetBrainsMono-Italic[wght].ttf");
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("https://cdn.sofiaritz.com/fonts/ofl/jetbrainsmono/JetBrainsMono[wght].ttf");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Rubik";
|
||||||
|
src: url("https://cdn.sofiaritz.com/fonts/ofl/rubik/Rubik[wght].ttf");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Rubik";
|
||||||
|
src: url("https://cdn.sofiaritz.com/fonts/ofl/rubik/Rubik-Italic[wght].ttf");
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
a {
|
||||||
|
@apply text-blue-700 underline hover:text-blue-800;
|
||||||
|
}
|
||||||
|
}
|
103
src/lib/api/index.ts
Normal file
103
src/lib/api/index.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import type {
|
||||||
|
DatabaseCreation,
|
||||||
|
DatabaseQuery,
|
||||||
|
EntityInfo,
|
||||||
|
Response,
|
||||||
|
UserInstanceData,
|
||||||
|
UserToken,
|
||||||
|
} from "./types"
|
||||||
|
import { DBType } from "./types"
|
||||||
|
|
||||||
|
async function request<T>(
|
||||||
|
endpoint: string,
|
||||||
|
auth: UserInstanceData,
|
||||||
|
{
|
||||||
|
headers: desiredHeaders = undefined,
|
||||||
|
text = undefined,
|
||||||
|
json = undefined,
|
||||||
|
method = undefined,
|
||||||
|
}: {
|
||||||
|
headers?: HeadersInit
|
||||||
|
text?: string
|
||||||
|
json?: any
|
||||||
|
method?: string
|
||||||
|
} = {},
|
||||||
|
): Promise<Response<T>> {
|
||||||
|
let url = new URL(auth.endpoint)
|
||||||
|
url.pathname = endpoint
|
||||||
|
|
||||||
|
let headers: any = {
|
||||||
|
...desiredHeaders,
|
||||||
|
}
|
||||||
|
if (auth.token != null && auth.token !== "undefined") {
|
||||||
|
headers["Authorization"] = `Bearer ${auth.token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let body
|
||||||
|
if (text != null && json != null) {
|
||||||
|
console.error(
|
||||||
|
`Both text and json are set in a request to ${auth.endpoint}. JSON will be used, this behaviour may change at any time!`,
|
||||||
|
)
|
||||||
|
body = JSON.stringify(json)
|
||||||
|
} else if (text != null) {
|
||||||
|
body = text
|
||||||
|
} else if (json != null) {
|
||||||
|
body = JSON.stringify(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await fetch(url, {
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
method,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unwrapResponse<T>(response: Response<T>): T {
|
||||||
|
if (isError(response)) {
|
||||||
|
throw response
|
||||||
|
} else {
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export const isError = (response: Response<any>): boolean => response?.["message"] != null
|
||||||
|
|
||||||
|
export async function login(entity: string, auth: UserInstanceData) {
|
||||||
|
await request("/v1/log_in", auth, {
|
||||||
|
headers: {
|
||||||
|
entity: entity,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirm(token: string, auth: UserInstanceData) {
|
||||||
|
return request<UserToken>("/v1/confirm", auth, {
|
||||||
|
headers: {
|
||||||
|
"authentication-token": token,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDatabase(slug: string, databaseType: DBType, auth: UserInstanceData) {
|
||||||
|
return request<DatabaseCreation>(`/v1/${slug}/create`, auth, {
|
||||||
|
headers: {
|
||||||
|
"db-type": databaseType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function entityInfo(entity: string, auth: UserInstanceData) {
|
||||||
|
return request<EntityInfo>(`/v1/entity/${entity}`, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queryDatabase(slug: string, query: string, auth: UserInstanceData) {
|
||||||
|
return request<DatabaseQuery>(`/v1/${slug}/query`, auth, {
|
||||||
|
text: query,
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
}
|
41
src/lib/api/types.ts
Normal file
41
src/lib/api/types.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
export type Response<T> = Error | T
|
||||||
|
|
||||||
|
export type UserInstanceData = {
|
||||||
|
endpoint: string
|
||||||
|
entity?: string
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DBType {
|
||||||
|
SQLite = "sqlite",
|
||||||
|
DuckDB = "duckdb",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Error = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DatabaseInfo = {
|
||||||
|
slug: string
|
||||||
|
database_type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityInfo = {
|
||||||
|
slug: string
|
||||||
|
databases: DatabaseInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserToken = {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DatabaseCreation = {
|
||||||
|
entity: string
|
||||||
|
database: string
|
||||||
|
database_type: DBType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DatabaseQuery = {
|
||||||
|
fields: string[]
|
||||||
|
rows: string[][]
|
||||||
|
}
|
57
src/lib/auth/stores.ts
Normal file
57
src/lib/auth/stores.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import type { EntityInfo, UserInstanceData } from "../api/types"
|
||||||
|
import { entityInfo, isError } from "../api"
|
||||||
|
|
||||||
|
const CREDENTIAL_STORAGE_KEY = "v1/auth.token"
|
||||||
|
|
||||||
|
export function parseCredentials(credentials: string): UserInstanceData {
|
||||||
|
let url = new URL(credentials)
|
||||||
|
return {
|
||||||
|
endpoint: url.origin,
|
||||||
|
entity: url.username && url.password ? url.username : undefined,
|
||||||
|
token: url.username && url.password ? url.password : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This store contains the _raw_ credentials. Use `userInstanceData` for usable data.
|
||||||
|
export const credentials = writable<string | null>(localStorage.getItem(CREDENTIAL_STORAGE_KEY))
|
||||||
|
/// This store is guaranteed to contain `entity` and `token`
|
||||||
|
export const userInstanceData = writable<UserInstanceData | undefined>()
|
||||||
|
export const userInfo = writable<EntityInfo | undefined>()
|
||||||
|
export const loggedIn = writable(false)
|
||||||
|
|
||||||
|
credentials.subscribe(async (v) => {
|
||||||
|
const resetStores = () => {
|
||||||
|
loggedIn.set(false)
|
||||||
|
userInfo.set(undefined)
|
||||||
|
userInstanceData.set(undefined)
|
||||||
|
localStorage.removeItem(CREDENTIAL_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
v == null ||
|
||||||
|
v.length === 0 ||
|
||||||
|
parseCredentials(v).entity == null ||
|
||||||
|
parseCredentials(v).token == null
|
||||||
|
) {
|
||||||
|
resetStores()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let instanceData = parseCredentials(v)
|
||||||
|
|
||||||
|
let data = await entityInfo(instanceData.entity!, instanceData)
|
||||||
|
if (isError(data)) {
|
||||||
|
console.error("Failed to retrieve entity info")
|
||||||
|
// TODO(sofiaritz): Improve error handling. Even though this case should *never* happen, we should be ready.
|
||||||
|
alert("Failed to retrieve entity info. Try again later")
|
||||||
|
resetStores()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data as EntityInfo // We have verified that this is not an error above.
|
||||||
|
userInstanceData.set(instanceData)
|
||||||
|
userInfo.set(data)
|
||||||
|
loggedIn.set(true)
|
||||||
|
localStorage.setItem(CREDENTIAL_STORAGE_KEY, v)
|
||||||
|
})
|
18
src/lib/components/Header.svelte
Normal file
18
src/lib/components/Header.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { loggedIn, userInfo } from "../auth/stores"
|
||||||
|
import { Link } from "svelte-routing"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="flex items-center justify-between rounded-b-2xl bg-gray-100 p-4">
|
||||||
|
<h1 class="text-xl">
|
||||||
|
<Link class="text-black no-underline hover:text-gray-800" to="/">aybWeb</Link>
|
||||||
|
</h1>
|
||||||
|
{#if $loggedIn}
|
||||||
|
<span>Logged in as <code>{$userInfo.slug}</code></span>
|
||||||
|
{:else}
|
||||||
|
<Link
|
||||||
|
class="rounded bg-blue-600 px-3 py-1.5 text-white no-underline hover:bg-blue-700 hover:text-white"
|
||||||
|
to="/auth/login">Login</Link
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</header>
|
7
src/lib/components/Redirect.svelte
Normal file
7
src/lib/components/Redirect.svelte
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { navigate } from "svelte-routing"
|
||||||
|
|
||||||
|
export let to: string = "/"
|
||||||
|
onMount(() => navigate(to))
|
||||||
|
</script>
|
8
src/lib/components/common/Button.svelte
Normal file
8
src/lib/components/common/Button.svelte
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let type: string = ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="rounded-lg bg-blue-700 px-5 py-3 text-center font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300"
|
||||||
|
{type}><slot /></button
|
||||||
|
>
|
18
src/lib/components/common/Input.svelte
Normal file
18
src/lib/components/common/Input.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let type: string = "text"
|
||||||
|
export let name: string = ""
|
||||||
|
export let id: string = ""
|
||||||
|
export let placeholder: string = ""
|
||||||
|
export let value: string = ""
|
||||||
|
export let disabled: boolean = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 disabled:bg-gray-100 disabled:font-mono"
|
||||||
|
{type}
|
||||||
|
{name}
|
||||||
|
{id}
|
||||||
|
{placeholder}
|
||||||
|
{value}
|
||||||
|
{disabled}
|
||||||
|
/>
|
2
src/lib/components/common/index.ts
Normal file
2
src/lib/components/common/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Button } from "./Button.svelte"
|
||||||
|
export { default as Input } from "./Input.svelte"
|
74
src/lib/components/database/DatabaseCard.svelte
Normal file
74
src/lib/components/database/DatabaseCard.svelte
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DBType } from "../../api/types";
|
||||||
|
import { Link } from "svelte-routing";
|
||||||
|
|
||||||
|
export let entity: string
|
||||||
|
export let slug: string
|
||||||
|
export let type: DBType
|
||||||
|
|
||||||
|
const displayType = (type: DBType) => {
|
||||||
|
if (type === DBType.SQLite) {
|
||||||
|
return "SQLite"
|
||||||
|
} else if (type === DBType.DuckDB) {
|
||||||
|
return "Postgres"
|
||||||
|
} else {
|
||||||
|
return "Unknown database type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const background = async (slug: string) => {
|
||||||
|
const utf8 = new TextEncoder().encode(slug);
|
||||||
|
let hash = await crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => {
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray
|
||||||
|
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
})
|
||||||
|
|
||||||
|
let hashCode = [...hash].reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0);
|
||||||
|
let primaryColor = (() => {
|
||||||
|
let c = (hashCode & 0x00FFFFFF)
|
||||||
|
.toString(16)
|
||||||
|
.toUpperCase()
|
||||||
|
|
||||||
|
return "00000".substring(0, 6 - c.length) + c
|
||||||
|
})()
|
||||||
|
|
||||||
|
let patternClass = (() => {
|
||||||
|
let firstNum = parseInt(primaryColor[3], 16)
|
||||||
|
return [
|
||||||
|
"pattern-checks-md",
|
||||||
|
"pattern-diagonal-stripes-md",
|
||||||
|
"pattern-dots-xl",
|
||||||
|
"pattern-dots-xl",
|
||||||
|
"pattern-horizontal-stripes-sm",
|
||||||
|
"pattern-vertical-stripes-md",
|
||||||
|
"pattern-triangles-lg",
|
||||||
|
"pattern-triangles-md",
|
||||||
|
"pattern-diagonal-lines-sm",
|
||||||
|
"pattern-cross-dots-xl",
|
||||||
|
"pattern-cross-dots-lg",
|
||||||
|
"pattern-grid-md",
|
||||||
|
"pattern-checks-lg",
|
||||||
|
"pattern-checks-sm",
|
||||||
|
"pattern-grid-sm",
|
||||||
|
"pattern-diagonal-stripes-md",
|
||||||
|
][firstNum]
|
||||||
|
})()
|
||||||
|
|
||||||
|
return [patternClass, `background-color: #${primaryColor}; color: white;`]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link to={`/database/${entity}/${slug}/overview`} role="button" class="border border-gray-300 rounded no-underline text-black hover:text-black">
|
||||||
|
{#await background(slug)}
|
||||||
|
<div class="h-24 pattern-checks-sm"></div>
|
||||||
|
{:then result}
|
||||||
|
<div class={`h-24 ${result[0]}`} style={result[1]}></div>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<span>{slug.split(".")[0]}</span>
|
||||||
|
</div>
|
||||||
|
<span class="block px-2 mb-2">Database type: {displayType(type)}</span>
|
||||||
|
</Link>
|
27
src/lib/components/database/DatabasePagesHeader.svelte
Normal file
27
src/lib/components/database/DatabasePagesHeader.svelte
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link } from "svelte-routing";
|
||||||
|
|
||||||
|
export let entity: string
|
||||||
|
export let slug: string
|
||||||
|
export let selected: "overview" | "query" | "settings" = "overview"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-32 gap-2">
|
||||||
|
{#if selected === "overview"}
|
||||||
|
<span>Overview</span>
|
||||||
|
{:else}
|
||||||
|
<Link to={`/database/${entity}/${slug}/overview`}>Overview</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selected === "query"}
|
||||||
|
<span>Query</span>
|
||||||
|
{:else}
|
||||||
|
<Link to={`/database/${entity}/${slug}/query`}>Query</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selected === "settings"}
|
||||||
|
<span>Settings</span>
|
||||||
|
{:else}
|
||||||
|
<Link to={`/database/${entity}/${slug}/settings`}>Settings</Link>
|
||||||
|
{/if}
|
||||||
|
</div>
|
24
src/lib/components/database/DatabaseQueryResult.svelte
Normal file
24
src/lib/components/database/DatabaseQueryResult.svelte
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DatabaseQuery } from "../../api/types";
|
||||||
|
|
||||||
|
export let data: DatabaseQuery
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<table class="border-gray-400 border p-3 w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-gray-400 border text-left">
|
||||||
|
{#each data.fields as field}
|
||||||
|
<th class="border-gray-400 border px-2">{field}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each data.rows as row}
|
||||||
|
<tr class="border-gray-400 border">
|
||||||
|
{#each row as value}
|
||||||
|
<td class="px-2 border-gray-400 border">{value}</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
9
src/main.ts
Normal file
9
src/main.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import "./app.css"
|
||||||
|
import "pattern.css/dist/pattern.min.css"
|
||||||
|
import App from "./App.svelte"
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.getElementById("app"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
80
src/routes/Confirm.svelte
Normal file
80
src/routes/Confirm.svelte
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Input from "../lib/components/common/Input.svelte"
|
||||||
|
import Button from "../lib/components/common/Button.svelte"
|
||||||
|
import { createForm } from "felte"
|
||||||
|
import { confirm, entityInfo, isError, unwrapResponse } from "../lib/api"
|
||||||
|
import { credentials, loggedIn } from "../lib/auth/stores"
|
||||||
|
import { navigate } from "svelte-routing"
|
||||||
|
|
||||||
|
export let token: string | undefined
|
||||||
|
|
||||||
|
let error
|
||||||
|
const { form } = createForm({
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
let response = unwrapResponse(
|
||||||
|
await confirm(values.token, {
|
||||||
|
endpoint: values["instance"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [values, response]
|
||||||
|
},
|
||||||
|
onSuccess: async (value: any) => {
|
||||||
|
let [values, data] = value
|
||||||
|
let response = await entityInfo(values["username"], {
|
||||||
|
endpoint: values["instance"],
|
||||||
|
entity: values["username"],
|
||||||
|
token: data["token"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError(response)) {
|
||||||
|
error = response
|
||||||
|
console.error(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = new URL(values["instance"])
|
||||||
|
url.username = values["username"]
|
||||||
|
url.password = data["token"]
|
||||||
|
|
||||||
|
credentials.set(url.toString())
|
||||||
|
navigate("/")
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
error = err
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="md:w-8/12">
|
||||||
|
<h1 class="text-2xl">Confirmation</h1>
|
||||||
|
<p class="text-xl text-gray-900">We are almost there...</p>
|
||||||
|
<form class="mt-5 flex flex-col gap-3.5" use:form>
|
||||||
|
<label class="block" for="instance-input">
|
||||||
|
Instance
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
name="instance"
|
||||||
|
id="instance-input"
|
||||||
|
placeholder="https://ayb.sofiaritz.com"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="block" for="username-input">
|
||||||
|
Username
|
||||||
|
<Input name="username" id="username-input" placeholder="alice" />
|
||||||
|
</label>
|
||||||
|
<label class="block" for="token-input">
|
||||||
|
Token
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
name="token"
|
||||||
|
id="token-input"
|
||||||
|
bind:value={token}
|
||||||
|
placeholder="gAAAAA(...)"
|
||||||
|
disabled={token != null && token.length > 0}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button type="submit">Confirm</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
32
src/routes/DatabaseOverview.svelte
Normal file
32
src/routes/DatabaseOverview.svelte
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import DatabasePagesHeader from "../lib/components/database/DatabasePagesHeader.svelte";
|
||||||
|
|
||||||
|
export let entity: string
|
||||||
|
export let slug: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-6 md:w-8/12">
|
||||||
|
<DatabasePagesHeader selected="overview" {slug} {entity}/>
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<h1 class="text-2xl">Congratulations, you've got your database set-up!</h1>
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<div class="border border-gray-300 rounded p-6 mt-6 w-full">
|
||||||
|
<h2 class="text-xl">Documentation</h2>
|
||||||
|
<span class="text-gray-500 block mt-1">Check out the link below for more information about ayb</span>
|
||||||
|
<a class="mt-1 block" href="https://git.sofiaritz.com/sofia/wip">ayb.host/docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 rounded p-6 mt-6 w-full">
|
||||||
|
<h2 class="text-xl">Getting help</h2>
|
||||||
|
<span class="text-gray-500 block mt-1">Contact the instance owner for support</span>
|
||||||
|
<a class="mt-1 block" href="https://git.sofiaritz.com/sofia/wip">ayb.host/support</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<div class="border border-gray-300 rounded p-6 mt-6 w-full">
|
||||||
|
<h2 class="text-xl">Usage limits</h2>
|
||||||
|
<span class="text-gray-500 block mt-1">You are not subject to usage limits right now</span>
|
||||||
|
<a class="mt-1 block" href="https://git.sofiaritz.com/sofia/wip">ayb.host/limits</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
79
src/routes/DatabaseQuery.svelte
Normal file
79
src/routes/DatabaseQuery.svelte
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { login, queryDatabase, unwrapResponse } from "../lib/api";
|
||||||
|
import { userInstanceData } from "../lib/auth/stores";
|
||||||
|
import DatabaseResult from "../lib/components/database/DatabaseQueryResult.svelte";
|
||||||
|
import DatabasePagesHeader from "../lib/components/database/DatabasePagesHeader.svelte";
|
||||||
|
import { createForm } from "felte";
|
||||||
|
import { Input } from "../lib/components/common";
|
||||||
|
import Button from "../lib/components/common/Button.svelte";
|
||||||
|
|
||||||
|
export let entity: string
|
||||||
|
export let slug: string
|
||||||
|
|
||||||
|
let result: any
|
||||||
|
let bigMode = false
|
||||||
|
|
||||||
|
const { form } = createForm({
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
let lowercaseQuery = values["query"].toLowerCase()
|
||||||
|
if (lowercaseQuery.includes("delete") || lowercaseQuery.includes("drop") || lowercaseQuery.includes("truncate")) {
|
||||||
|
let confirmed = confirm("Your query can potentially perform destructive actions. Do you want to continue?")
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = { status: "loading" }
|
||||||
|
let query = values["query"].trim()
|
||||||
|
if (!query.endsWith(";")) query += ";";
|
||||||
|
|
||||||
|
return await queryDatabase(`${entity}/${slug}`, query, $userInstanceData)
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
result = data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-6 md:w-8/12">
|
||||||
|
<DatabasePagesHeader selected="query" {slug} {entity}/>
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<h1 class="text-2xl">Start querying your database</h1>
|
||||||
|
<div class="flex gap-2 pt-4">
|
||||||
|
<label for="big-mode mb-0">Big mode enabled</label>
|
||||||
|
<input id="big-mode" type="checkbox" bind:checked={bigMode} />
|
||||||
|
</div>
|
||||||
|
<form class="mt-2 gap-6" use:form>
|
||||||
|
{#if bigMode === true}
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<textarea name="query" rows="5" placeholder="SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
score
|
||||||
|
FROM favorite_databases"/>
|
||||||
|
<Button type="submit">Query</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<Input name="query" type="text" placeholder="SELECT * FROM favorite_databases"/>
|
||||||
|
<Button type="submit">Query</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
{#if result != null}
|
||||||
|
{#if result.message != null}
|
||||||
|
<span><span class="pr-2 text-red-600">Error</span>{result.message}</span>
|
||||||
|
{:else if ((result.fields != null && result.rows != null) && result.rows.length > 0)}
|
||||||
|
<DatabaseResult data={result} />
|
||||||
|
{:else if (result.fields != null && result.rows != null)}
|
||||||
|
<span class="text-gray-500 block">No rows returned</span>
|
||||||
|
{:else if result.status === "loading"}
|
||||||
|
<span class="text-gray-500 block">Loading...</span>
|
||||||
|
{:else}
|
||||||
|
<span><span class="pr-2 text-red-600">Error</span>Unknown response from the server</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="text-gray-500 block">Waiting for your query...</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
25
src/routes/Home.svelte
Normal file
25
src/routes/Home.svelte
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { userInfo } from "../lib/auth/stores";
|
||||||
|
import DatabaseCard from "../lib/components/database/DatabaseCard.svelte";
|
||||||
|
import { Link } from "svelte-routing";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="md:w-8/12">
|
||||||
|
<h1 class="text-2xl">Your databases</h1>
|
||||||
|
|
||||||
|
{#if $userInfo.databases.length > 0}
|
||||||
|
<div class="grid grid-cols-2 gap-6 mt-5">
|
||||||
|
{#each $userInfo.databases as database (database.slug)}
|
||||||
|
<DatabaseCard entity={$userInfo.slug} slug={database.slug} type={database.database_type}/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex mt-5">
|
||||||
|
<Link to="/database/new" role="button" class="flex flex-col gap-3 justify-center items-center h-60 w-full border border-gray-300 rounded p-2 no-underline text-black hover:text-black">
|
||||||
|
<span class="text-4xl">+</span>
|
||||||
|
<h2 class="font-bold text-xl">Create a new database</h2>
|
||||||
|
<span class="text-gray-500 block">The instance owner hasn't set any limits for your account</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
75
src/routes/Login.svelte
Normal file
75
src/routes/Login.svelte
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createForm } from "felte"
|
||||||
|
import { Button, Input } from "../lib/components/common"
|
||||||
|
import { login } from "../lib/api"
|
||||||
|
import { unwrapResponse } from "../lib/api"
|
||||||
|
import { Link } from "svelte-routing"
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Waiting,
|
||||||
|
EmailSent,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = State.Waiting
|
||||||
|
let error: any
|
||||||
|
|
||||||
|
const { form } = createForm({
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
return unwrapResponse(
|
||||||
|
await login(values.username, {
|
||||||
|
endpoint: values["instance"],
|
||||||
|
entity: values["username"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
state = State.EmailSent
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
error = err
|
||||||
|
state = State.Error
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="md:w-8/12">
|
||||||
|
{#if state === State.EmailSent}
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Check your mailbox</h1>
|
||||||
|
<div>
|
||||||
|
<p>We have sent you an e-mail with the confirmation link.</p>
|
||||||
|
<p>
|
||||||
|
You may received a CLI command instead, if that's the case follow this link: <Link
|
||||||
|
to="/auth/confirm">/auth/confirm</Link
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if state === State.Error}
|
||||||
|
<h1 class="text-2xl text-red">Error</h1>
|
||||||
|
<span>{error.toString()}</span>
|
||||||
|
{:else if state === State.Waiting}
|
||||||
|
<h1 class="mb-6 text-2xl font-bold text-gray-900">Login</h1>
|
||||||
|
<form class="flex flex-col gap-3.5" use:form>
|
||||||
|
<label class="block" for="instance-input">
|
||||||
|
Instance
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
name="instance"
|
||||||
|
id="instance-input"
|
||||||
|
placeholder="https://ayb.sofiaritz.com"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700"
|
||||||
|
>You can find an updated instance list at <a
|
||||||
|
href="https://git.sofiaritz.com/sofia/wip">ayb.host/instances</a
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="block" for="username-input">
|
||||||
|
Username
|
||||||
|
<Input name="username" id="username-input" placeholder="alice" />
|
||||||
|
</label>
|
||||||
|
<Button type="submit">Send login link</Button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
2
src/vite-env.d.ts
vendored
Normal file
2
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
7
svelte.config.js
Normal file
7
svelte.config.js
Normal 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(),
|
||||||
|
}
|
77
tailwind.config.js
Normal file
77
tailwind.config.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// Flexoki tailwind theme: https://gist.github.com/martin-mael/4b50fa8e55da846f3f73399d84fa1848
|
||||||
|
const colors = {
|
||||||
|
base: {
|
||||||
|
black: "#100F0F",
|
||||||
|
950: "#1C1B1A",
|
||||||
|
900: "#282726",
|
||||||
|
850: "#343331",
|
||||||
|
800: "#403E3C",
|
||||||
|
700: "#575653",
|
||||||
|
600: "#6F6E69",
|
||||||
|
500: "#878580",
|
||||||
|
300: "#B7B5AC",
|
||||||
|
200: "#CECDC3",
|
||||||
|
150: "#DAD8CE",
|
||||||
|
100: "#E6E4D9",
|
||||||
|
50: "#F2F0E5",
|
||||||
|
paper: "#FFFCF0",
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
DEFAULT: "#AF3029",
|
||||||
|
light: "#D14D41",
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
DEFAULT: "#BC5215",
|
||||||
|
light: "#DA702C",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
DEFAULT: "#AD8301",
|
||||||
|
light: "#D0A215",
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
DEFAULT: "#66800B",
|
||||||
|
light: "#879A39",
|
||||||
|
},
|
||||||
|
cyan: {
|
||||||
|
DEFAULT: "#24837B",
|
||||||
|
light: "#3AA99F",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
DEFAULT: "#205EA6",
|
||||||
|
light: "#4385BE",
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
DEFAULT: "#5E409D",
|
||||||
|
light: "#8B7EC8",
|
||||||
|
},
|
||||||
|
magenta: {
|
||||||
|
DEFAULT: "#A02F6F",
|
||||||
|
light: "#CE5D97",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontFamily = {
|
||||||
|
sans: [
|
||||||
|
"Rubik",
|
||||||
|
"ui-sans-serif",
|
||||||
|
"system-ui",
|
||||||
|
"sans-serif",
|
||||||
|
'"Apple Color Emoji"',
|
||||||
|
'"Segoe UI Emoji"',
|
||||||
|
'"Segoe UI Symbol"',
|
||||||
|
'"Noto Color Emoji"',
|
||||||
|
],
|
||||||
|
mono: ['"Jetbrains Mono"', "monospace"],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,svelte}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors,
|
||||||
|
fontFamily,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
20
tsconfig.json
Normal file
20
tsconfig.json
Normal 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
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
})
|
Loading…
Reference in a new issue