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