refs #0 The root commit.
This commit is contained in:
1
front/.env
Normal file
1
front/.env
Normal file
@@ -0,0 +1 @@
|
||||
BACKEND_URL="http://app:8000"
|
||||
17
front/package.json
Normal file
17
front/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "library",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"svelte": "^3.54.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
12
front/src/app.html
Normal file
12
front/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
26
front/src/lib/components/AddForm.svelte
Normal file
26
front/src/lib/components/AddForm.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script>
|
||||
export let item, project, title;
|
||||
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
import Field from './Field.svelte';
|
||||
import { getKeys, handleCreateItem } from './util.js';
|
||||
|
||||
$: keys = getKeys(item);
|
||||
|
||||
</script>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" use:enhance>
|
||||
<table>
|
||||
<tbody>
|
||||
{#each keys as key}
|
||||
<tr><td>
|
||||
<Field project={project} item={item} key={key}/>
|
||||
</td></tr>
|
||||
{/each}
|
||||
<tr><td>
|
||||
<button on:click|preventDefault={handleCreateItem(title)}>Save</button>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
35
front/src/lib/components/EditForm.svelte
Normal file
35
front/src/lib/components/EditForm.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script>
|
||||
export let item, project, title;
|
||||
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
import Field from '$lib/components/Field.svelte';
|
||||
import FileField from '$lib/components/FileField.svelte';
|
||||
|
||||
import { getKeys, getType, handleUpdateItem, handleDeleteItem } from './util.js';
|
||||
|
||||
$: keys = getKeys(item);
|
||||
|
||||
</script>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" use:enhance>
|
||||
<table>
|
||||
<tbody>
|
||||
{#each keys as key}
|
||||
<tr><td>
|
||||
{#if getType(project, key) == 'file'}
|
||||
<FileField title={title} item={item} key={key}/>
|
||||
{:else}
|
||||
<Field project={project} item={item} key={key}/>
|
||||
{/if}
|
||||
</td></tr>
|
||||
{/each}
|
||||
<tr><td>
|
||||
<button on:click|preventDefault={handleUpdateItem(title, item.id)}>Save</button>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<button on:click|preventDefault={handleDeleteItem(title, item.id)}>Delete</button>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
17
front/src/lib/components/Field.svelte
Normal file
17
front/src/lib/components/Field.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export let item, key, project;
|
||||
|
||||
import { capitalize, getType, getValue } from './util.js';
|
||||
|
||||
$: type = getType(project, key);
|
||||
$: value = getValue(item, key);
|
||||
$: label = capitalize(key);
|
||||
</script>
|
||||
|
||||
<label for={key}>{label}:</label>
|
||||
<input
|
||||
name={key}
|
||||
value={value}
|
||||
autocomplete="off"
|
||||
type={type}
|
||||
/>
|
||||
21
front/src/lib/components/FileField.svelte
Normal file
21
front/src/lib/components/FileField.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
export let item, key, title;
|
||||
|
||||
import { capitalize, getFileName, getValue, handleDeleteFile } from './util.js';
|
||||
|
||||
$: label = capitalize(key);
|
||||
$: value = getValue(item, key);
|
||||
$: fileName = getFileName(value);
|
||||
</script>
|
||||
|
||||
<label for={key}>{label}:</label>
|
||||
<input
|
||||
name={key}
|
||||
autocomplete="off"
|
||||
type="file"
|
||||
id={key}
|
||||
/>
|
||||
{#if value}
|
||||
<a href="/{title}/{item.id}/{key}" target="_blank">{fileName}</a>
|
||||
<button on:click|preventDefault={handleDeleteFile(title, item.id, key)}>Delete File</button>
|
||||
{/if}
|
||||
12
front/src/lib/components/HeadRow.svelte
Normal file
12
front/src/lib/components/HeadRow.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export let keys;
|
||||
|
||||
import { capitalize } from './util.js';
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td></td>
|
||||
{#each keys as key}
|
||||
<td>{capitalize(key)}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
21
front/src/lib/components/Row.svelte
Normal file
21
front/src/lib/components/Row.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
export let title, item, project;
|
||||
|
||||
import { getKeys, getValue, getType, getFileName } from './util.js';
|
||||
|
||||
$: keys = getKeys(item);
|
||||
</script>
|
||||
|
||||
|
||||
<tr>
|
||||
<td><a href="/{title}/{item.id}">{item.id}</a></td>
|
||||
{#each keys as key}
|
||||
<td>
|
||||
{#if getType(project, key) == 'file'}
|
||||
{getFileName(getValue(item, key) || "")}
|
||||
{:else}
|
||||
{getValue(item, key) || ""}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
20
front/src/lib/components/Table.svelte
Normal file
20
front/src/lib/components/Table.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
export let items, title, project;
|
||||
|
||||
import HeadRow from './HeadRow.svelte';
|
||||
import Row from './Row.svelte';
|
||||
import { getKeys } from './util.js';
|
||||
</script>
|
||||
|
||||
{#if items.length !== 0}
|
||||
<table>
|
||||
<thead>
|
||||
<HeadRow keys={getKeys(items[0])}/>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each items as item (item.id)}
|
||||
<Row project={project} item={item} title={title}/>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
85
front/src/lib/components/util.js
Normal file
85
front/src/lib/components/util.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
|
||||
export function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
};
|
||||
|
||||
export function getFileName(str) {
|
||||
if (str !== null) {
|
||||
return str.replace(/^.*[\\\/]/, '');
|
||||
}
|
||||
};
|
||||
|
||||
export function getKeys(item) {
|
||||
return Object.keys(item).filter(key => key !== 'id');
|
||||
};
|
||||
|
||||
export function getValue(item, key) {
|
||||
return item[key];
|
||||
};
|
||||
|
||||
export function getTableField(project, item, key) {
|
||||
const value = getValue(item, key) || '';
|
||||
const type = getType(project, key);
|
||||
if (type == 'file') {
|
||||
return getFileName(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export function getType(project, key) {
|
||||
const type = project.fields[key].type;
|
||||
const type_map = {
|
||||
str: 'text',
|
||||
int: 'number',
|
||||
file: 'file'
|
||||
}
|
||||
return type_map[type]
|
||||
};
|
||||
|
||||
export async function handleCreateItem(project) {
|
||||
const form = document.querySelector('form');
|
||||
const data = new FormData(form);
|
||||
const response = await fetch(
|
||||
`/${project}/add`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: data,
|
||||
}
|
||||
)
|
||||
const item = await response.json();
|
||||
goto(`/${project}/${item.id}`, {invalidateAll: true});
|
||||
};
|
||||
|
||||
export async function handleUpdateItem(project, item_id) {
|
||||
const form = document.querySelector('form');
|
||||
const data = new FormData(form);
|
||||
const response = await fetch(
|
||||
`/${project}/${item_id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
}
|
||||
);
|
||||
invalidate('data:item');
|
||||
invalidate('data:items');
|
||||
};
|
||||
|
||||
export async function handleDeleteItem(project, item_id) {
|
||||
const response = await fetch(
|
||||
`/${project}/${item_id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
goto(`/${project}`, {invalidateAll: true});
|
||||
};
|
||||
|
||||
|
||||
export async function handleDeleteFile(project, item_id, field) {
|
||||
const response = await fetch(`/${project}/${item_id}/${field}`, {method: 'DELETE'})
|
||||
document.querySelector(`input[name=${field}]`).value = "";
|
||||
invalidate('data:item');
|
||||
invalidate('data:items');
|
||||
};
|
||||
147
front/src/lib/server/database.js
Normal file
147
front/src/lib/server/database.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { error, fail } from '@sveltejs/kit';
|
||||
import { BACKEND_URL } from '$env/static/private';
|
||||
|
||||
const module = "book"
|
||||
|
||||
export async function getItems(project) {
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getItem(project, id) {
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/${id}`);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
const item = await response.json();
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function getEmptyItem(project) {
|
||||
const response = await fetch(`${BACKEND_URL}/_module/${project}`);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
const result = await response.json();
|
||||
const fields = Object.keys(result.fields);
|
||||
const entries = new Map(fields.map(field => [field, null]))
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
export async function getProjects() {
|
||||
const response = await fetch(`${BACKEND_URL}/_module/`);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getProject(project) {
|
||||
const response = await fetch(`${BACKEND_URL}/_module/${project}`);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
|
||||
export async function createItem(project, data) {
|
||||
const fields = Object.fromEntries(
|
||||
Array.from(data.entries()).filter(
|
||||
([key, value]) => !(value instanceof File)
|
||||
)
|
||||
);
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(fields)
|
||||
}
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/`, options);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
|
||||
const item = await response.json();
|
||||
|
||||
const files = Object.fromEntries(
|
||||
Array.from(data.entries()).filter(
|
||||
([key, value]) => value instanceof File && value.size !== 0
|
||||
)
|
||||
);
|
||||
|
||||
for (let key in files)
|
||||
{
|
||||
const data = new FormData()
|
||||
data.append('file', files[key]);
|
||||
await uploadFile(project, item["id"], key, data);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function updateItem(project, id, data) {
|
||||
const files = Object.fromEntries(
|
||||
Array.from(data.entries()).filter(
|
||||
([key, value]) => value instanceof File && value.size !== 0
|
||||
)
|
||||
);
|
||||
|
||||
for (let key in files)
|
||||
{
|
||||
const data = new FormData()
|
||||
data.append('file', files[key]);
|
||||
await deleteFile(project, id, key);
|
||||
await uploadFile(project, id, key, data);
|
||||
}
|
||||
|
||||
const fields = Object.fromEntries(
|
||||
Array.from(data.entries()).filter(
|
||||
([key, value]) => !(value instanceof File)
|
||||
)
|
||||
);
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(fields)
|
||||
}
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/${id}`, options);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
|
||||
const item = response.json();
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function deleteItem(project, id) {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/${id}`, options);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function readFile(project, id, field) {
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/${id}/${field}`);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function uploadFile(project, id, field, data) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
}
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/${id}/${field}`, options);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteFile(project, id, field) {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}
|
||||
const response = await fetch(`${BACKEND_URL}/${project}/${id}/${field}`, options);
|
||||
if (response.status !== 200) throw error(response.status);
|
||||
return response.json();
|
||||
}
|
||||
8
front/src/routes/+layout.server.js
Normal file
8
front/src/routes/+layout.server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { getProjects } from '$lib/server/database.js';
|
||||
|
||||
export async function load() {
|
||||
const projects = await getProjects();
|
||||
return {
|
||||
projects: projects
|
||||
};
|
||||
}
|
||||
12
front/src/routes/+layout.svelte
Normal file
12
front/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<a href="/">home</a> |
|
||||
{#each data.projects as project}
|
||||
<a href="/{project}">{project}</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<slot/>
|
||||
1
front/src/routes/+page.svelte
Normal file
1
front/src/routes/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>home</h1>
|
||||
16
front/src/routes/[project]/+layout.server.js
Normal file
16
front/src/routes/[project]/+layout.server.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getItems, getProject, getProjects } from '$lib/server/database.js';
|
||||
|
||||
export async function load({ params, depends }) {
|
||||
depends('data:items');
|
||||
|
||||
const projects = await getProjects();
|
||||
if (!projects.includes(params.project)) throw error(404);
|
||||
const items = await getItems(params.project);
|
||||
const project = await getProject(params.project);
|
||||
return {
|
||||
title: params.project,
|
||||
project: project,
|
||||
items: items
|
||||
};
|
||||
}
|
||||
7
front/src/routes/[project]/+layout.svelte
Normal file
7
front/src/routes/[project]/+layout.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<h1>{data.title}</h1>
|
||||
|
||||
<slot/>
|
||||
12
front/src/routes/[project]/+page.svelte
Normal file
12
front/src/routes/[project]/+page.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import { enhance } from '$app/forms';
|
||||
import Table from '$lib/components/Table.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<Table project={data.project} items={data.items} title={data.title}/>
|
||||
|
||||
<form action="/{data.title}/add" method="GET">
|
||||
<input type="submit" value="Add"/>
|
||||
</form>
|
||||
0
front/src/routes/[project]/Page.svelte
Normal file
0
front/src/routes/[project]/Page.svelte
Normal file
8
front/src/routes/[project]/[id]/+page.server.js
Normal file
8
front/src/routes/[project]/[id]/+page.server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { getItem } from '$lib/server/database.js';
|
||||
|
||||
export async function load({ params, depends }) {
|
||||
depends('data:item');
|
||||
|
||||
const item = await getItem(params.project, params.id);
|
||||
return { item };
|
||||
}
|
||||
7
front/src/routes/[project]/[id]/+page.svelte
Normal file
7
front/src/routes/[project]/[id]/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import EditForm from '$lib/components/EditForm.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<EditForm project={data.project} title={data.title} item={data.item}/>
|
||||
14
front/src/routes/[project]/[id]/+server.js
Normal file
14
front/src/routes/[project]/[id]/+server.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { deleteItem, updateItem } from '$lib/server/database.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function PUT({ params, request }) {
|
||||
const updateData = await request.formData();
|
||||
const item = await updateItem(params.project, params.id, updateData);
|
||||
return json(item);
|
||||
}
|
||||
|
||||
export async function DELETE({ params }) {
|
||||
const item = await deleteItem(params.project, params.id);
|
||||
return json(item);
|
||||
}
|
||||
12
front/src/routes/[project]/[id]/[field]/+server.js
Normal file
12
front/src/routes/[project]/[id]/[field]/+server.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { readFile, deleteFile } from '$lib/server/database.js';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function DELETE({params}) {
|
||||
const response = await deleteFile(params.project, params.id, params.field);
|
||||
return json(response);
|
||||
}
|
||||
|
||||
export async function GET({params}) {
|
||||
const response = await readFile(params.project, params.id, params.field);
|
||||
return new Response(response.body);
|
||||
}
|
||||
8
front/src/routes/[project]/add/+page.server.js
Normal file
8
front/src/routes/[project]/add/+page.server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { getEmptyItem } from '$lib/server/database.js';
|
||||
|
||||
export async function load({ params }) {
|
||||
const item = await getEmptyItem(params.project);
|
||||
|
||||
return { item };
|
||||
}
|
||||
8
front/src/routes/[project]/add/+page.svelte
Normal file
8
front/src/routes/[project]/add/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
import { enhance } from '$app/forms';
|
||||
import AddForm from '$lib/components/AddForm.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<AddForm project={data.project} title={data.title} item={data.item}/>
|
||||
8
front/src/routes/[project]/add/+server.js
Normal file
8
front/src/routes/[project]/add/+server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createItem } from '$lib/server/database.js';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function POST({ params, request }) {
|
||||
const data = await request.formData();
|
||||
const item = await createItem(params.project, data);
|
||||
return json(item);
|
||||
}
|
||||
BIN
front/static/favicon.png
Normal file
BIN
front/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
10
front/svelte.config.js
Normal file
10
front/svelte.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
6
front/vite.config.js
Normal file
6
front/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Reference in New Issue
Block a user