This commit is contained in:
Ewen 2023-09-13 22:53:44 +02:00
parent c742b244b4
commit b5ce0124a0
10 changed files with 372 additions and 68 deletions

View file

@ -11,7 +11,9 @@
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.4.2",
"@sveltejs/adapter-auto": "^2.1.0", "@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.24.1", "@sveltejs/kit": "^1.24.1",
"axios": "^1.5.0" "axios": "^1.5.0",
"svelte-forms-lib": "^2.0.1",
"yup": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.2", "@sveltejs/vite-plugin-svelte": "^2.4.2",
@ -1756,6 +1758,11 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "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" "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": { "node_modules/svelte-hmr": {
"version": "0.15.3", "version": "0.15.3",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
@ -2226,6 +2241,11 @@
"node": ">=0.8" "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": { "node_modules/tiny-glob": {
"version": "0.2.9", "version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
@ -2247,6 +2267,11 @@
"node": ">=8.0" "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": { "node_modules/totalist": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@ -2267,6 +2292,17 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true "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": { "node_modules/typescript": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
@ -2408,6 +2444,17 @@
"engines": { "engines": {
"node": ">= 14" "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"
}
} }
} }
} }

View file

@ -25,6 +25,8 @@
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.4.2",
"@sveltejs/adapter-auto": "^2.1.0", "@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.24.1", "@sveltejs/kit": "^1.24.1",
"axios": "^1.5.0" "axios": "^1.5.0",
"svelte-forms-lib": "^2.0.1",
"yup": "^1.2.0"
} }
} }

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

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

View file

@ -1,14 +1,102 @@
<script lang="ts"> <script lang="ts">
import { afterUpdate, createEventDispatcher } from "svelte";
import type { Level } from "./types"; import type { Level } from "./types";
import LevelTag from "./LevelTag.svelte"; 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 word: string;
export let definition: string; export let definition: string;
export let level: Level; 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> </script>
<div class=""> <tr class="text-slate-700 border-b">
<LevelTag {level} /> <td class="p-2">
</div> {#if !isEditLevel}
<div>{word}</div> <LevelTag {level} on:click={toggleEditLevel} />
<div>{definition}</div> {: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>

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

View file

@ -2,80 +2,197 @@
import { page } from "$app/stores"; import { page } from "$app/stores";
import axios from "$lib/api.ts"; import axios from "$lib/api.ts";
import WordForm from "$lib/WordForm.svelte"; 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 { 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[]; let words: Word[];
const wordResponse = axios.get("words/").then((data) => { $: words;
words = snakeToCamelCase(data.data); 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; let isFormNewShown = false;
function toggleFormNew() { function toggleFormNew() {
isFormNewShown = !isFormNewShown; isFormNewShown = !isFormNewShown;
} }
function newWord(e) { function submitNewWord(values) {
const formData = new FormData(e.target); const formData = {
level_id: values.level,
word: values.word,
definition: values.definition,
};
const data = {}; try {
for (let field of formData) { axios
const [key, value] = field; .post("words/", formData)
data[key] = value; .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> </script>
<main> <main>
{#await wordResponse} {#await promise}
<p></p> <p></p>
{:then} {:then}
<div class="grid grid-cols-3 gap-y-2 gap-x-1"> <form on:submit={handleSubmit}>
<div class="font-bold">Live</div> <table class="w-full text-left">
<div class="font-bold">Ger</div> <thead>
<div class="font-bold">Termenadur</div> <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} <tbody>
<div /> {#if !isFormNewShown}
<button <tr>
type="button" <td colspan="4" class="text-center p-2 border-b">
class="bg-violet-600 hover:bg-violet-700 rounded-md text-white p-1 w-8 text-center" <ButtonMain onClick={toggleFormNew}>
on:click={toggleFormNew} <span
> class="fa-regular fa-plus align-top"
<span class="fa-regular fa-plus" /> />
</button> </ButtonMain>
<div /> </td>
{:else} </tr>
<form on:submit|preventDefault={newWord}> {:else}
<div> <tr class="border-b">
<input <td class="p-2">
name="level" <select
placeholder="Live" id="level"
class="p-1 rounded-md border w-1/2" name="level"
/> on:change={handleChange}
</div> bind:value={$form.level}
<div> class="p-1 rounded-md border"
<input required
name="word" >
placeholder="Ger" {#await promiseLevels}<option></option>
class="p-1 rounded-md border w-full" {:then}
/> {#each levels as level}
</div> <option value={level.levelNumber}
<div> >{level.levelString}</option
<input >
name="definition" {/each}
placeholder="Termenadur" {/await}
class="p-1 rounded-md border w-full" </select>
/> </td>
</div> <td class="p-2">
</form> <input
{/if} name="word"
placeholder="Ger"
{#each words as word} on:change={handleChange}
<WordForm {...word} /> bind:value={$form.word}
{/each} class="p-1 rounded-md border w-full"
</div> 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} {/await}
</main> </main>

View file

@ -1,4 +1,4 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import { vitePreprocess } from '@sveltejs/kit/vite'
import adapter from "@sveltejs/adapter-auto" import adapter from "@sveltejs/adapter-auto"

View file

@ -1,5 +1,5 @@
{ {
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
@ -15,6 +15,15 @@
"checkJs": true, "checkJs": true,
"isolatedModules": true "isolatedModules": true
}, },
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": [
"references": [{ "path": "./tsconfig.node.json" }] "src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.svelte"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
} }

View file

@ -10,10 +10,22 @@ class LevelSerializer(serializers.ModelSerializer):
class WordSerializer(serializers.ModelSerializer): class WordSerializer(serializers.ModelSerializer):
level = LevelSerializer(many=False, read_only=True) level = LevelSerializer(many=False, read_only=True)
level_id = serializers.PrimaryKeyRelatedField(
write_only=True, source="level", queryset=Level.objects.all()
)
class Meta: class Meta:
model = Word 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): class GridSerializer(serializers.ModelSerializer):