Skip to content

Commit

Permalink
Merge pull request #15 from ZeitOnline/ZO-6269-zod-validation
Browse files Browse the repository at this point in the history
ZO-6269: zod validation
  • Loading branch information
manuelsanchez2 authored Sep 30, 2024
2 parents 2d728bd + 6f9aed8 commit c7d0af0
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 149 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules
# OS
.DS_Store
Thumbs.db
.cursorrules

# Env
.env
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,12 @@ You can run some tests by using the following commands:
Check the main task on [Jira](https://zeit-online.atlassian.net/browse/ZO-5839) for further information.

- [ ] Implement the Microsoft Authentication
- [ ] POST game (works but the id generation is manually done instead of through the db). Here we have the problem that when we submit the game, we are sending one only request to create the game and the questions (not yet the game_state). Since we cannot create the questions without the game_id, that is why we do the id generation manually. However, we might need to refactor to do 2 requests.
- [ ] POST questions
- [ ] Validation is required (for the moment, we can create games without questions)
- [x] POST game (works but the id generation is manually done instead of through the db). Here we have the problem that when we submit the game, we are sending one only request to create the game and the questions (not yet the game_state). Since we cannot create the questions without the game_id, that is why we do the id generation manually. However, we might need to refactor to do 2 requests.
- [x] POST questions
- [x] Validation is required (for the moment, we can create games without questions)
- [ ] PATCH game and questions do not work as expected (they are creating instead of UPSERT) and you need to update the page to make it work.
- [ ] We have used superform in NewGameView.svelte in two forms. Do we need it also when updating game and deleting?
- [x] We have used superform in NewGameView.svelte in two forms. Do we need it also when updating game and deleting?
- [ ] DELETE game with CASCADE of the game_question and game_state
- [ ] Tests for the different views need to be written.
- [ ] beforeleave message (when clicking back in theb browser oder zueruck in the buttons so that user does not lose information if there was)
- [ ] whole logs topic (need to be discussed) + logs table sort + pagination
- [x] beforeleave message (when clicking back in theb browser oder zueruck in the buttons so that user does not lose information if there was)
- [ ] adr (pr started)
178 changes: 105 additions & 73 deletions src/components/AddGameTable.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<script lang="ts">
import { formFieldProxy, superForm, arrayProxy } from 'sveltekit-superforms';
import { formFieldProxy, superForm, arrayProxy, setError } from 'sveltekit-superforms';
import type { PageData } from '../routes/$types';
import { toast } from '@zerodevx/svelte-toast';
import Separator from './Separator.svelte';
import { blur } from 'svelte/transition';
import ErrorIcon from '$components/icons/HasErrorIcon.svelte';
import IconHandler from './icons/IconHandler.svelte';
import { cubicInOut } from 'svelte/easing';
import { createGame, createGameQuestions, getNextAvailableDateForGame } from '$lib/queries';
import { dev } from '$app/environment';
import ViewNavigation from './ViewNavigation.svelte';
import GameCell from './GameCell.svelte';
import { Orientation, type Question } from '$types';
import { zodClient } from 'sveltekit-superforms/adapters';
import { saveGameFormSchema } from '../schemas/generate-game';
let {
resultsDataBody = $bindable(),
Expand All @@ -28,42 +28,47 @@
const superform = superForm(
data.saveGameForm,
{
validators: false,
validators: zodClient(saveGameFormSchema),
SPA: true,
taintedMessage: isSubmitted ? false : true,
// onChange(event) {
// if (dev) {
// if (event.target) {
// // Form input event
// console.log(event.path, 'was changed with', event.target, 'in form', event.formElement);
// } else {
// // Programmatic event
// console.log('Fields updated:', event.paths);
// if (event.target) {
// if(event.path === 'name') {
// const isNameTaken = data.games.some((game: any) => game.name === $form.name)
// console.log('we are changing the name')
// } else if (event.path === 'release_date') {
// const isDateTaken = data.games.some((game: any) => game.release_date === $form.release_date)
// console.log('we are changing the date')
// }
// console.log('release date:', $form.release_date);
// }
// },
async onUpdate({ form }) {
try {
// Fetch the highest existing game ID asynchronously
// TODO: incremental ID is working, but here we are doing it like this
// because the updateGame is asking for it before
// we need to separate the logic and do different requests for games and questions
if (data.games.some((game: any) => game.name === $form.name)) {
setError(form, 'name', 'This name is already taken');
}
if (data.games.some((game: any) => game.release_date === $form.release_date)) {
setError(form, 'release_date', 'There is already a game on this day');
}
// Construct the data for the new game
const data = {
const finalData = {
name: $form.name,
release_date: $form.release_date,
active: $form.published,
};
// Log the new game data to be added
console.log('Adding new game:', data);
console.log('Adding new game:', finalData);
if (!form.valid) {
return;
}
// Send the new game data to the backend
const newGameArray = await createGame(data);
const newGameArray = await createGame(finalData);
const newGame = newGameArray[0];
newGame.questions = $form.questions;
newGame.questions.map((question) => {
Expand All @@ -73,6 +78,8 @@
if (!resp.ok) {
throw new Error('Failed to add questions');
}
} catch (error) {
// TODO: Error handling for conflict 409/500 etc
console.error('Error adding game:', error);
Expand Down Expand Up @@ -108,7 +115,7 @@
);
const { form, message, constraints, errors, enhance, isTainted, reset } = superform;
const { path, value } = formFieldProxy(superform, 'name');
const { path: pathQuestions, values: questionValues } = arrayProxy(superform, 'questions');
const { path: pathQuestions, values: questionValues, valueErrors: questionErrors } = arrayProxy(superform, 'questions');
// Function to add a new row
function addRow() {
Expand Down Expand Up @@ -186,27 +193,6 @@
}
});
let customNameError = $state(false);
// TODO: change this to server validation
$effect(() => {
if (data.games.some((game: any) => game.name === $form.name)) {
customNameError = true;
} else {
customNameError = false;
}
});
let customDateError = $state(false);
$effect(() => {
if (data.games.some((game: any) => game.release_date === $form.release_date)) {
customDateError = true;
} else {
customDateError = false;
}
});
function resetAll() {
reset();
resultsDataBody = [];
Expand Down Expand Up @@ -249,18 +235,15 @@
id="game_name"
type="text"
placeholder="GameXXXX"
aria-invalid={$errors.name || customNameError ? 'true' : undefined}
aria-invalid={$errors.name ? 'true' : undefined}
bind:value={$value}
/>
{#if $errors.name}<span style="color: red;" class="invalid">{$errors.name}</span>{/if}
{#if customNameError}<div
in:blur
style="color: red;"
class="invalid flex items-center gap-z-ds-4 absolute -bottom-6 left-0 text-xs"
>
{#if $errors.name}
<div in:blur class="text-red-500 invalid flex items-center gap-z-ds-4 absolute -bottom-6 left-0 text-xs">
<IconHandler iconName="error" extraClasses="w-4 h-4 text-z-ds-color-accent-100" />
<span class="text-nowrap text-xs">This name is already taken</span>
</div>{/if}
<span class="text-nowrap text-xs">{$errors.name}</span>
</div>
{/if}
</div>
</div>

Expand All @@ -275,20 +258,16 @@
name="release_date"
id="release_date"
type="date"
aria-invalid={$errors.release_date || customDateError ? 'true' : undefined}
aria-invalid={$errors.release_date ? 'true' : undefined}
bind:value={$form.release_date}
/>
{#if $errors.release_date}<span style="color: red;" class="invalid"
>{$errors.release_date}</span
>{/if}
{#if customDateError}<div
in:blur
style="color: red;"
class="invalid flex items-center gap-z-ds-4 absolute -bottom-6 left-0 text-xs"
>
<ErrorIcon extraClasses="w-4 h-4 text-z-ds-color-accent-100" />
<span class="text-nowrap text-xs">There is already a game on this day</span>
</div>{/if}
{#if $errors.release_date}
<div in:blur class="text-red-500 invalid flex items-center gap-z-ds-4 absolute -bottom-6 left-0 text-xs">
<IconHandler iconName="error" extraClasses="w-4 h-4 text-z-ds-color-accent-100" />
<span class="text-nowrap text-xs">{$errors.release_date}</span>
</div>
{/if}

</div>
</div>

Expand All @@ -308,9 +287,10 @@

<div class="flex justify-between items-center w-full gap-z-ds-8 my-z-ds-24">
<div class="font-bold text-nowrap">Fragen des Spiels</div>

</div>

<div class="relative overflow-x-auto">
<div class="relative overflow-x-auto overflow-y-visible">
<table class="w-full text-sm text-left rtl:text-right text-gray-900">
<thead>
<tr>
Expand All @@ -337,13 +317,26 @@
in:blur={{ duration: 300, delay: 0, easing: cubicInOut }}
out:blur={{ duration: 300, delay: 0, easing: cubicInOut }}
>
<GameCell bind:dataToBind={$questionValues[i].nr} />
<GameCell bind:dataToBind={$questionValues[i].question} />
<GameCell bind:dataToBind={$questionValues[i].answer} />
<GameCell bind:dataToBind={$questionValues[i].xc} />
<GameCell bind:dataToBind={$questionValues[i].yc} />
<GameCell bind:dataToBind={$questionValues[i].direction} />
<GameCell bind:dataToBind={$questionValues[i].description} />
<td>
<input type="number" class="w-full bg-transparent" aria-invalid={$questionErrors?.[i]?.nr ? 'true' : undefined} bind:value={$questionValues[i].nr} />
<!-- {#if $questionErrors?.[i]?.nr}
{@const error = $questionErrors?.[i]?.nr}
{/if} -->
</td>
<GameCell bind:dataToBind={$questionValues[i].question} error={$questionErrors?.[i]?.question} />
<GameCell bind:dataToBind={$questionValues[i].answer} error={$questionErrors?.[i]?.answer} />
<td>
<input type="number" class="w-full bg-transparent" aria-invalid={$questionErrors?.[i]?.xc ? 'true' : undefined} bind:value={$questionValues[i].xc} />

</td>
<td>
<input type="number" class="w-full bg-transparent" aria-invalid={$questionErrors?.[i]?.yc ? 'true' : undefined} bind:value={$questionValues[i].yc} />
<!-- {#if $questionErrors?.[i]?.yc}
<span class="text-red-500 invalid text-xs">{$questionErrors?.[i]?.yc}</span>
{/if} -->
</td>
<GameCell bind:dataToBind={$questionValues[i].direction} error={$questionErrors?.[i]?.direction} />
<GameCell bind:dataToBind={$questionValues[i].description} error={$questionErrors?.[i]?.description} />

<td class="!border-0">
<button
Expand All @@ -355,12 +348,51 @@
-
</button>
</td>


</tr>
{/each}
</tbody>
</table>
</div>

{#if $questionErrors.some((error) => error.nr || error.xc || error.yc || error.direction || error.description)}

<div role="alert" aria-atomic="true" class="flex flex-col justify-center mx-auto mt-12 w-fit border-red-500 border text-red-500 p-4">

<div class="flex items-center gap-3 mb-3">
<IconHandler iconName="error" extraClasses="w-4 h-4 text-z-ds-color-accent-100" />
<span id="error-heading">Bitte, korrigieren Sie die Fehler hier:</span>
</div>


<ul aria-live="assertive" class="flex flex-col justify-center list-inside list-disc max-w-[300px]" aria-labelledby="error-heading">

{#each $questionErrors as _i, i}
{#if $questionErrors?.[i]?.nr}
<li class="px-2 text-sm">
[R: {i + 1}] - {$questionErrors?.[i]?.nr}
</li>

{/if}
{#if $questionErrors?.[i]?.xc}
<li class="px-2 text-sm">
[R: {i + 1}] - {$questionErrors?.[i]?.xc}

</li>
{/if}
{#if $questionErrors?.[i]?.yc}
<li class="px-2 text-sm">
[R: {i + 1}] - {$questionErrors?.[i]?.yc}
</li>
{/if}
{/each}
</ul>
</div>

{/if}


<div class="flex flex-row gap-4 items-center my-12 mx-auto w-full justify-center">
<button class="z-ds-button" type="submit">Neues Spiel erstellen</button>
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/components/GameCell.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<script lang="ts">
let { dataToBind = $bindable() }: { dataToBind: any } = $props();
let { dataToBind = $bindable(), error }: { dataToBind: any, error: string | undefined } = $props();
</script>

<td>
<textarea class="w-full bg-transparent" bind:value={dataToBind}></textarea>
{#if error}
<span class="text-red-500 invalid text-xs">{error}</span>
{/if}
</td>

<style>
Expand Down
Loading

0 comments on commit c7d0af0

Please sign in to comment.