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",
"@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"
}
}
}
}

View file

@ -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"
}
}

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

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

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"

View file

@ -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"
}
]
}

View file

@ -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):