wip
This commit is contained in:
parent
c742b244b4
commit
b5ce0124a0
49
frontend/package-lock.json
generated
49
frontend/package-lock.json
generated
|
@ -11,7 +11,9 @@
|
|||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@sveltejs/adapter-auto": "^2.1.0",
|
||||
"@sveltejs/kit": "^1.24.1",
|
||||
"axios": "^1.5.0"
|
||||
"axios": "^1.5.0",
|
||||
"svelte-forms-lib": "^2.0.1",
|
||||
"yup": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
|
@ -1756,6 +1758,11 @@
|
|||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
|
||||
"integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
@ -2071,6 +2078,14 @@
|
|||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-forms-lib": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-forms-lib/-/svelte-forms-lib-2.0.1.tgz",
|
||||
"integrity": "sha512-kwbJ3ynsepsrrJyAMrvSc0Lj/myc9vfI2DL8OKxgArZimrNYsRh1gENYhvrcKEI3BiZrv8q3VFfmGo/GMyk7Zg==",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hmr": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
|
@ -2226,6 +2241,11 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
|
||||
},
|
||||
"node_modules/tiny-glob": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||
|
@ -2247,6 +2267,11 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
|
@ -2267,6 +2292,17 @@
|
|||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
|
@ -2408,6 +2444,17 @@
|
|||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz",
|
||||
"integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@sveltejs/adapter-auto": "^2.1.0",
|
||||
"@sveltejs/kit": "^1.24.1",
|
||||
"axios": "^1.5.0"
|
||||
"axios": "^1.5.0",
|
||||
"svelte-forms-lib": "^2.0.1",
|
||||
"yup": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
11
frontend/src/lib/ButtonMain.svelte
Normal file
11
frontend/src/lib/ButtonMain.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
export let onClick = (e?) => {};
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="bg-violet-600 hover:bg-violet-700 rounded-md text-white p-1 w-6 h-6 justify-center"
|
||||
on:click|preventDefault={onClick}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
11
frontend/src/lib/ButtonTrash.svelte
Normal file
11
frontend/src/lib/ButtonTrash.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
export let onClick = (e?) => {};
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="bg-red-600 hover:bg-red-700 rounded-md text-white p-1 w-6 h-6 justify-center"
|
||||
on:click|preventDefault={onClick}
|
||||
>
|
||||
<span class="fa-solid fa-trash align-top" />
|
||||
</button>
|
|
@ -1,14 +1,102 @@
|
|||
<script lang="ts">
|
||||
import { afterUpdate, createEventDispatcher } from "svelte";
|
||||
import type { Level } from "./types";
|
||||
import LevelTag from "./LevelTag.svelte";
|
||||
import ButtonMain from "./ButtonMain.svelte";
|
||||
import ButtonTrash from "./ButtonTrash.svelte";
|
||||
import axios from "$lib/api.ts";
|
||||
|
||||
export let id: number;
|
||||
export let word: string;
|
||||
export let definition: string;
|
||||
export let level: Level;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function deleteWord() {
|
||||
dispatch("deleteWord");
|
||||
}
|
||||
|
||||
let isEditLevel = false;
|
||||
let isEditWord = false;
|
||||
let isEditDefinition = false;
|
||||
|
||||
function toggleEditLevel() {
|
||||
isEditLevel = true;
|
||||
}
|
||||
|
||||
function toggleEditWord() {
|
||||
isEditWord = true;
|
||||
}
|
||||
function toggleEditDefinition() {
|
||||
isEditDefinition = true;
|
||||
}
|
||||
|
||||
function init(el) {
|
||||
el.focus();
|
||||
}
|
||||
|
||||
function updateWord() {
|
||||
try {
|
||||
axios.patch("words/" + id + "/", { word: word });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
function updateDefinition() {
|
||||
try {
|
||||
axios.patch("words/" + id + "/", { definition: definition });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<LevelTag {level} />
|
||||
</div>
|
||||
<div>{word}</div>
|
||||
<div>{definition}</div>
|
||||
<tr class="text-slate-700 border-b">
|
||||
<td class="p-2">
|
||||
{#if !isEditLevel}
|
||||
<LevelTag {level} on:click={toggleEditLevel} />
|
||||
{:else}
|
||||
<select />
|
||||
{/if}
|
||||
</td>
|
||||
<td class="p-2">
|
||||
{#if !isEditWord}
|
||||
<div
|
||||
class="p-1 w-full hover:cursor-pointer"
|
||||
on:click={toggleEditWord}
|
||||
>
|
||||
{word}
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
name="word"
|
||||
bind:value={word}
|
||||
class="p-1 rounded-md border w-full"
|
||||
use:init
|
||||
on:change={updateWord}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="p-2">
|
||||
{#if !isEditDefinition}
|
||||
<div
|
||||
class="w-full hover:cursor-pointer"
|
||||
on:click={toggleEditDefinition}
|
||||
>
|
||||
{definition}
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
name="definition"
|
||||
bind:value={definition}
|
||||
on:change={updateDefinition}
|
||||
use:init
|
||||
class="p-1 rounded-md border w-full"
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<ButtonTrash onClick={deleteWord} />
|
||||
</td>
|
||||
</tr>
|
||||
|
|
7
frontend/src/lib/arrays.ts
Normal file
7
frontend/src/lib/arrays.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function deleteFromArray(arr: any[], obj: any): any[] {
|
||||
const index = arr.indexOf(obj, 0);
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
return arr;
|
||||
}
|
|
@ -2,80 +2,197 @@
|
|||
import { page } from "$app/stores";
|
||||
import axios from "$lib/api.ts";
|
||||
import WordForm from "$lib/WordForm.svelte";
|
||||
import { type Word } from "../../lib/types";
|
||||
import ButtonMain from "$lib/ButtonMain.svelte";
|
||||
import type { Level, Word } from "../../lib/types";
|
||||
import { deleteFromArray } from "$lib/arrays";
|
||||
|
||||
import { snakeToCamelCase } from "$lib/case";
|
||||
import { onMount, tick } from "svelte";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { createForm } from "svelte-forms-lib";
|
||||
import * as yup from "yup";
|
||||
|
||||
let words: Word[];
|
||||
const wordResponse = axios.get("words/").then((data) => {
|
||||
words = snakeToCamelCase(data.data);
|
||||
$: words;
|
||||
let levels: Level[];
|
||||
let promise;
|
||||
let promiseLevels;
|
||||
|
||||
// Form
|
||||
const { form, errors, state, handleChange, handleSubmit } = createForm({
|
||||
initialValues: {
|
||||
level: "",
|
||||
word: "",
|
||||
definition: "",
|
||||
},
|
||||
validationSchema: yup.object().shape({
|
||||
level: yup.number().required(),
|
||||
word: yup.string().required(),
|
||||
definition: yup.string().required(),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
submitNewWord(values);
|
||||
},
|
||||
});
|
||||
|
||||
function fetchLevels() {
|
||||
try {
|
||||
promiseLevels = axios.get("levels/").then((data) => {
|
||||
levels = snakeToCamelCase(data.data);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchData() {
|
||||
try {
|
||||
promise = axios.get("words/").then((data) => {
|
||||
words = snakeToCamelCase(data.data);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
let isFormNewShown = false;
|
||||
|
||||
function toggleFormNew() {
|
||||
isFormNewShown = !isFormNewShown;
|
||||
}
|
||||
|
||||
function newWord(e) {
|
||||
const formData = new FormData(e.target);
|
||||
function submitNewWord(values) {
|
||||
const formData = {
|
||||
level_id: values.level,
|
||||
word: values.word,
|
||||
definition: values.definition,
|
||||
};
|
||||
|
||||
const data = {};
|
||||
for (let field of formData) {
|
||||
const [key, value] = field;
|
||||
data[key] = value;
|
||||
try {
|
||||
axios
|
||||
.post("words/", formData)
|
||||
.then((data) => {
|
||||
words = [snakeToCamelCase(data.data), ...words];
|
||||
})
|
||||
.then(() => {
|
||||
$form.definition = "";
|
||||
$form.word = "";
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function deleteWord(word: Word) {
|
||||
if (!(word && word.id)) return;
|
||||
try {
|
||||
axios.delete("/words/" + word.id).then(() => {
|
||||
words = deleteFromArray(words, word);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchData();
|
||||
fetchLevels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
{#await wordResponse}
|
||||
{#await promise}
|
||||
<p>…</p>
|
||||
{:then}
|
||||
<div class="grid grid-cols-3 gap-y-2 gap-x-1">
|
||||
<div class="font-bold">Live</div>
|
||||
<div class="font-bold">Ger</div>
|
||||
<div class="font-bold">Termenadur</div>
|
||||
<form on:submit={handleSubmit}>
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr
|
||||
class="bg-slate-100 rounded-lg font-bold text-slate-700 border-separate border-b"
|
||||
>
|
||||
<th class="font-bold p-2">Live</th>
|
||||
<th class="font-bold p-2">Ger</th>
|
||||
<th class="font-bold p-2">Termenadur</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{#if !isFormNewShown}
|
||||
<div />
|
||||
<button
|
||||
type="button"
|
||||
class="bg-violet-600 hover:bg-violet-700 rounded-md text-white p-1 w-8 text-center"
|
||||
on:click={toggleFormNew}
|
||||
>
|
||||
<span class="fa-regular fa-plus" />
|
||||
</button>
|
||||
<div />
|
||||
{:else}
|
||||
<form on:submit|preventDefault={newWord}>
|
||||
<div>
|
||||
<input
|
||||
name="level"
|
||||
placeholder="Live"
|
||||
class="p-1 rounded-md border w-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
name="word"
|
||||
placeholder="Ger"
|
||||
class="p-1 rounded-md border w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
name="definition"
|
||||
placeholder="Termenadur"
|
||||
class="p-1 rounded-md border w-full"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#each words as word}
|
||||
<WordForm {...word} />
|
||||
{/each}
|
||||
</div>
|
||||
<tbody>
|
||||
{#if !isFormNewShown}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center p-2 border-b">
|
||||
<ButtonMain onClick={toggleFormNew}>
|
||||
<span
|
||||
class="fa-regular fa-plus align-top"
|
||||
/>
|
||||
</ButtonMain>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr class="border-b">
|
||||
<td class="p-2">
|
||||
<select
|
||||
id="level"
|
||||
name="level"
|
||||
on:change={handleChange}
|
||||
bind:value={$form.level}
|
||||
class="p-1 rounded-md border"
|
||||
required
|
||||
>
|
||||
{#await promiseLevels}<option>…</option>
|
||||
{:then}
|
||||
{#each levels as level}
|
||||
<option value={level.levelNumber}
|
||||
>{level.levelString}</option
|
||||
>
|
||||
{/each}
|
||||
{/await}
|
||||
</select>
|
||||
</td>
|
||||
<td class="p-2">
|
||||
<input
|
||||
name="word"
|
||||
placeholder="Ger"
|
||||
on:change={handleChange}
|
||||
bind:value={$form.word}
|
||||
class="p-1 rounded-md border w-full"
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td class="p-2">
|
||||
<input
|
||||
name="definition"
|
||||
placeholder="Termenadur"
|
||||
on:change={handleChange}
|
||||
bind:value={$form.definition}
|
||||
class="p-1 rounded-md border w-full"
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td class="p-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-violet-600 hover:bg-violet-700 rounded-md text-white p-1 w-6 h-6 justify-center"
|
||||
>
|
||||
<span
|
||||
class="fa-regular fa-plus align-top"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if words}
|
||||
{#each words as word}
|
||||
<WordForm
|
||||
{...word}
|
||||
on:deleteWord={deleteWord(word)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{:catch error}
|
||||
{error}
|
||||
{/await}
|
||||
</main>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite'
|
||||
import adapter from "@sveltejs/adapter-auto"
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
|
@ -15,6 +15,15 @@
|
|||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
"include": [
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.js",
|
||||
"src/**/*.svelte"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -10,10 +10,22 @@ class LevelSerializer(serializers.ModelSerializer):
|
|||
|
||||
class WordSerializer(serializers.ModelSerializer):
|
||||
level = LevelSerializer(many=False, read_only=True)
|
||||
level_id = serializers.PrimaryKeyRelatedField(
|
||||
write_only=True, source="level", queryset=Level.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Word
|
||||
fields = ["id", "word", "definition", "level"]
|
||||
fields = ["id", "word", "definition", "level", "level_id"]
|
||||
|
||||
# def create(self, validated_data):
|
||||
# print(validated_data)
|
||||
# level_data = validated_data.pop("level")
|
||||
# level = Level.objects.get(pk=level_data)
|
||||
|
||||
# word = Word.objects.create(level=level, **validated_data)
|
||||
|
||||
# return word
|
||||
|
||||
|
||||
class GridSerializer(serializers.ModelSerializer):
|
||||
|
|
Loading…
Reference in a new issue