Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add election editing #650

Merged
merged 11 commits into from
Dec 16, 2024
3 changes: 3 additions & 0 deletions src/lib/utils/apiNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const apiNames = {
UPDATE: "governing_document:write",
DELETE: "governing_document:write",
},
ELECTION: {
...crud("election"),
},
DOOR: {
...crud("core:access:door"),
},
Expand Down
15 changes: 15 additions & 0 deletions src/routes/(app)/elections/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,25 @@ export const load: PageServerLoad = async ({ locals }) => {
link: true,
expiresAt: true,
committee: true,
id: true,
},
});

const committees = await prisma.committee.findMany({
orderBy: [
{
name: "asc",
},
],
select: {
id: true,
name: true,
nameEn: true,
},
});

return {
openElections,
committees,
};
};
27 changes: 20 additions & 7 deletions src/routes/(app)/elections/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
import * as m from "$paraglide/messages";
import MarkdownBody from "$lib/components/MarkdownBody.svelte";
import { languageTag } from "$paraglide/runtime";
import { isAuthorized } from "$lib/utils/authorization";
import apiNames from "$lib/utils/apiNames";
import CommitteeIcon from "$lib/components/images/CommitteeIcon.svelte";
export let data: PageData;
</script>

<PageHeader title={m.openElections()} />
<!-- TODO: make this editable by board members with Markdown page -->
<div class="flex flex-row">
<PageHeader title={m.openElections()} />
{#if isAuthorized(apiNames.ELECTION.CREATE, data.user)}
<a href="/elections/create" class="btn btn-primary ml-auto">+ Nytt val</a>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to create since that's more consistent with our other pages (except for one for some reason...).

{/if}
</div>

<section class="mb-5 space-y-5">
<p>
{m.elections_description()}
Expand All @@ -19,11 +27,16 @@
{#each data.openElections as election}
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<img
class="w-5/12 self-center text-center"
src={election.committee.darkImageUrl}
alt="Committee logo"
/>
{#if isAuthorized(apiNames.ELECTION.UPDATE, data.user)}
<a
href={"/elections/" + election.id + "/edit"}
class="btn btn-outline btn-sm ml-auto"
Copy link
Member

@danieladugyan danieladugyan Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a lot of purple (btn-secondary) buttons on the page, but I'm not a big fan of them so I changed this one to a more neutral color.

I'm not really an expert on UI design (clearly... 😄), but colors tend to draw attention and usually signify some kind of main action we want the user to take. Editing is more of an optional action and thus doesn't need to be highlighted.

>Redigera
</a>
{/if}
<div class="w-5/12 self-center text-center">
<CommitteeIcon committee={election.committee} />
</div>

<h2 class="card-title self-center text-2xl font-bold">
{election.committee.name}
Expand Down
87 changes: 87 additions & 0 deletions src/routes/(app)/elections/ElectionEditor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang="ts">
import Input from "$lib/components/Input.svelte";
import type { SuperValidated } from "sveltekit-superforms";
import { superForm } from "$lib/utils/client/superForms";
import type { ElectionSchema } from "./schemas";
import * as m from "$paraglide/messages";

export let isCreating: boolean;
export let data: {
form: SuperValidated<ElectionSchema>;
committees: Array<{ id: string; name: string; nameEn: string | null }>;
election: {
markdown: string;
markdownEn: string | null;
link: string;
expiresAt: Date;
committeeId: string;
};
};
const { form, errors, constraints, enhance } = superForm(data.form);
</script>

<form
id="election-editor"
method="POST"
action={isCreating ? "?/create" : "?/update"}
use:enhance
class="flex flex-col gap-3"
>
<h1 class="text-2xl font-bold">
{isCreating ? m.elections_create() : m.elections_edit()}
</h1>
<Input
name="markdown"
label={m.elections_content_sv()}
required
textarea
bind:value={$form.markdown}
error={$errors.markdown}
{...$constraints.markdown}
/>
<Input
name="markdownEn"
label={m.elections_content_en()}
required
textarea
bind:value={$form.markdownEn}
error={$errors.markdownEn}
{...$constraints.markdownEn}
/>
<Input
name="link"
label={m.elections_link()}
required
bind:value={$form.link}
error={$errors.link}
{...$constraints.link}
/>
<Input
name="expiresAt"
label={m.elections_expiryDate()}
type="date"
required
bind:value={$form.expiresAt}
error={$errors.expiresAt}
{...$constraints.expiresAt}
/>

<label class="label-text" for="committee">{m.elections_committee()}*</label>
<select
id="committee"
name="committeeId"
class="max-w select select-bordered w-full"
bind:value={$form.committeeId}
{...$constraints.committeeId}
>
{#each data.committees as committeeOption}
<option value={committeeOption.id}>{committeeOption.name}</option>
{/each}
</select>
{#if $errors.committeeId}
<p class="text-error">{$errors.committeeId}</p>
{/if}
<button type="submit" class="btn btn-primary mx-auto mt-4 w-4/12">
{m.elections_save()}
</button>
</form>
73 changes: 73 additions & 0 deletions src/routes/(app)/elections/[id]/edit/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { error, fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms/server";
import { zod } from "sveltekit-superforms/adapters";
import { redirect } from "$lib/utils/redirect";
import { electionSchema } from "../../schemas";
import * as m from "$paraglide/messages";
import dayjs from "dayjs";

export const load: PageServerLoad = async ({ locals, params }) => {
const { prisma } = locals;
const electionPromise = prisma.election.findFirst({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of instantly awaiting the Promise that prisma.election.findFirst returns, I allow the second query to start before awaiting the result. This allows the queries to run at the same time.

where: { id: params.id },
});

const committees = await prisma.committee.findMany({
orderBy: [{ shortName: "asc" }],
select: {
id: true,
name: true,
nameEn: true,
},
});

const election = await electionPromise;

if (!election) {
throw error(404, m.elections_notFound());
}

return {
election,
committees,
form: await superValidate(
{
...election,
expiresAt: dayjs(election.expiresAt).format("YYYY-MM-DD"),
},
Comment on lines +35 to +38
Copy link
Member

@danieladugyan danieladugyan Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By passing an object into the superValidate call, we can easily set default values for a form. Unfortunately it's a bit more long-winded here since we need to transform election.expiresAt into a valid date string.

zod(electionSchema),
),
};
};

export const actions: Actions = {
update: async (event) => {
const { request, locals, params } = event;
const { prisma } = locals;
const form = await superValidate(request, zod(electionSchema));
if (!form.valid) return fail(400, { form });
const id = params.id;
const { markdown, markdownEn, link, expiresAt, committeeId } = form.data;
await prisma.election.update({
where: {
id,
},
data: {
markdown,
markdownEn,
link,
expiresAt: dayjs(expiresAt).endOf("day").toDate(),
committeeId,
},
});
throw redirect(
"/elections",
{
message: m.elections_updated(),
type: "success",
},
event,
);
},
};
10 changes: 10 additions & 0 deletions src/routes/(app)/elections/[id]/edit/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import SetPageTitle from "$lib/components/nav/SetPageTitle.svelte";
import ElectionEditor from "../../ElectionEditor.svelte";

import type { PageData } from "./$types";
export let data: PageData;
</script>

<SetPageTitle title={data.form.data.markdown} />
<ElectionEditor isCreating={false} {data} />
62 changes: 62 additions & 0 deletions src/routes/(app)/elections/create/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms/server";
import { zod } from "sveltekit-superforms/adapters";
import { redirect } from "$lib/utils/redirect";
import { electionSchema } from "../schemas";
import * as m from "$paraglide/messages";
import dayjs from "dayjs";

export const load: PageServerLoad = async ({ locals }) => {
const { prisma } = locals;

const committees = await prisma.committee.findMany({
orderBy: [{ shortName: "asc" }],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed some whitespace because Prisma query options are already quite long as it is. Note that this is one of the rare cases where our formatter Prettier allows you to choose how something should be formatted.

select: {
id: true,
name: true,
nameEn: true,
},
});

const election = {
markdown: "",
markdownEn: null,
link: "",
expiresAt: dayjs().endOf("day").toDate(),
committeeId: "",
};

return {
committees,
election,
form: await superValidate(zod(electionSchema)),
};
};

export const actions: Actions = {
create: async (event) => {
const { request, locals } = event;
const { prisma } = locals;
const form = await superValidate(request, zod(electionSchema));
if (!form.valid) return fail(400, { form });
const { markdown, markdownEn, link, expiresAt, committeeId } = form.data;
await prisma.election.create({
data: {
markdown,
markdownEn,
link,
expiresAt: dayjs(expiresAt).endOf("day").toDate(),
committeeId,
},
});
throw redirect(
"/elections",
{
message: m.elections_created(),
type: "success",
},
event,
);
},
};
10 changes: 10 additions & 0 deletions src/routes/(app)/elections/create/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import SetPageTitle from "$lib/components/nav/SetPageTitle.svelte";
import ElectionEditor from "../ElectionEditor.svelte";

import type { PageData } from "./$types";
export let data: PageData;
</script>

<SetPageTitle title={data.form.data.markdown} />
<ElectionEditor isCreating {data} />
11 changes: 11 additions & 0 deletions src/routes/(app)/elections/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Infer } from "sveltekit-superforms";
import { z } from "zod";

export const electionSchema = z.object({
markdown: z.string(),
markdownEn: z.string().nullable(),
link: z.string(),
expiresAt: z.string().date(),
committeeId: z.string().uuid(),
});
export type ElectionSchema = Infer<typeof electionSchema>;
11 changes: 11 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,17 @@
"elections_description": "Here are the application forms for all committees that currently have positions open for application. The requirement profiles and post descriptions are located in the application forms.",
"elections_close": "The elections close at",
"elections_apply": "Apply",
"elections_notFound": "Elections not found",
"elections_updated": "Election updated",
"elections_created": "Election created",
"elections_create": "Create election",
"elections_edit": "Edit election",
"elections_content_sv": "Content (SV)",
"elections_content_en": "Content (EN)",
"elections_link": "Link",
"elections_expiryDate": "Last application date (23:59)",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to this to make it more concise, I think it should already be pretty obvious that the last application date is inclusive.

"elections_committee": "Committee",
"elections_save": "Save",
"expenses": "Expenses",
"expense": "Expense",
"expenseCreated": "Expense created",
Expand Down
11 changes: 11 additions & 0 deletions src/translations/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,17 @@
"elections_description": "Här finns ansökningslänkar till de utskott som just nu har öppna val. Postbeskrivningar och kravprofiler finns i ansökningsformuläret.",
"elections_close": "Valet stänger",
"elections_apply": "Ansök",
"elections_notFound": "Valet hittades inte",
"elections_updated": "Val uppdaterat",
"elections_created": "Val skapat",
"elections_create": "Skapa val",
"elections_edit": "Redigera val",
"elections_content_sv": "Innehåll",
"elections_content_en": "Innehåll (EN)",
"elections_link": "Länk",
"elections_expiryDate": "Sista ansökningsdag (23:59)",
"elections_committee": "Utskott",
"elections_save": "Spara",
"expenses": "Utlägg",
"expense": "Utlägg",
"expenseCreated": "Utlägg skapat",
Expand Down
Loading