aybWeb (again)

This commit is contained in:
Sofía Aritz 2023-12-08 18:43:56 +01:00
commit ebd9f7ae36
Signed by: sofia
GPG key ID: 90B5116E3542B28F
35 changed files with 2247 additions and 0 deletions

7
.editorconfig Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
README.md Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

0
public/.gitkeep Normal file
View file

40
src/App.svelte Normal file
View 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
View 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
View 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
View 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
View 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)
})

View 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>

View 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>

View 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
>

View 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}
/>

View file

@ -0,0 +1,2 @@
export { default as Button } from "./Button.svelte"
export { default as Input } from "./Input.svelte"

View 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>

View 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>

View 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
View 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
View 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>

View 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>

View 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
View 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
View 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
View file

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

7
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(),
}

77
tailwind.config.js Normal file
View 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
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
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
tsconfig.node.json Normal file
View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View 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()],
})

1227
yarn.lock Normal file

File diff suppressed because it is too large Load diff