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

Division Documents #64

Merged
merged 17 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ model NewsPost {
}

model DivisionGroup {
id Int @id @default(autoincrement())
gammaSuperGroupId String @unique
slug String @unique
id Int @id @default(autoincrement())
gammaSuperGroupId String @unique
slug String @unique
prettyName String
descriptionSv String
descriptionEn String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
NewsPost NewsPost[]
DivisionPages DivisionPage[]
DivisionDocument DivisionDocument[]
}

model DivisionPage {
Expand Down Expand Up @@ -83,12 +84,28 @@ model Sponsor {
type SponsorType @default(PARTNER)
}

model DivisionDocument {
id Int @id @default(autoincrement())
DivisionGroup DivisionGroup @relation(fields: [divisionGroupId], references: [id])
divisionGroupId Int
media Media @relation(fields: [mediaSha256], references: [sha256])
mediaSha256 String
titleSv String
titleEn String
descriptionSv String
descriptionEn String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type DocumentType @default(MISC)
}

model Media {
sha256 String @id
extension String
createdAt DateTime @default(now())
NewsPost NewsPost[]
Sponsor Sponsor[]
sha256 String @id
extension String
createdAt DateTime @default(now())
NewsPost NewsPost[]
Sponsor Sponsor[]
DivisionDocument DivisionDocument[]
}

model EventNotifiers {
Expand All @@ -98,6 +115,15 @@ model EventNotifiers {
language Language
}

enum DocumentType {
PROTOCOL
BUDGET
BUSINESS_PLAN
FINANCIAL_REPORT
BUSINESS_REPORT
MISC
}

enum NotifierType {
DISCORD
SLACK
Expand Down
47 changes: 47 additions & 0 deletions src/actions/documents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use server';

import MediaService from '@/services/mediaService';
import DivisionDocumentService from '@/services/divisionDocumentService';
import { redirect } from 'next/navigation';
import { DocumentType } from '@prisma/client';
import { MediaType } from '@/services/fileService';
import SessionService from '@/services/sessionService';

export async function addDocument(
divisionSuperGroupId: string,
titleSv: string,
titleEn: string,
descriptionSv: string,
descriptionEn: string,
form: FormData,
type?: DocumentType
) {
if (!(await SessionService.canEditGroup(divisionSuperGroupId))) {
throw new Error('Unauthorized');
}

const file: File | null = form.get('file') as unknown as File;

if (!file) {
return;
}

const mediaId = (await MediaService.save(file, [MediaType.Document]))?.sha256;
if (mediaId) {
await DivisionDocumentService.add(
divisionSuperGroupId,
titleSv,
titleEn,
descriptionSv,
descriptionEn,
mediaId,
type
);
}
redirect('/documents');
}

export async function deleteDocument(documentId: number) {
await DivisionDocumentService.remove(documentId);
redirect('/documents');
}
32 changes: 32 additions & 0 deletions src/app/[locale]/documents/DeleteDocumentButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { deleteDocument } from '@/actions/documents';
import ActionButton from '@/components/ActionButton/ActionButton';
import { toast } from 'react-toastify';
import i18nService from '@/services/i18nService';

interface DeleteDocumentButtonProps {
id: number;
locale: string;
}

const DeleteDocumentButton = ({ id, locale }: DeleteDocumentButtonProps) => {
const l = i18nService.getLocale(locale);

async function remove() {
try {
confirm(l.docs.confirmDelete) &&
(await toast.promise(deleteDocument(id), {
pending: l.docs.deleting,
success: l.docs.deleted,
error: l.docs.deleteError
}));
} catch {
console.log('Failed to remove document');
}
}

return <ActionButton onClick={remove}>{l.general.delete}</ActionButton>;
};

export default DeleteDocumentButton;
122 changes: 122 additions & 0 deletions src/app/[locale]/documents/new/AddDocumentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use client';
import React, { useState } from 'react';
import { addDocument } from '@/actions/documents';
import DropdownList from '@/components/DropdownList/DropdownList';
import { GammaGroup } from '@/types/gamma';
import TextArea from '@/components/TextArea/TextArea';
import { DocumentType } from '@prisma/client';
import DivisionDocumentService from '@/services/divisionDocumentService';
import ActionButton from '@/components/ActionButton/ActionButton';
import FileService, { MediaType } from '@/services/fileService';
import i18nService from '@/services/i18nService';
import { toast } from 'react-toastify';

const validMimes = FileService.getValidMimes([MediaType.Document]);

const AddDocumentForm = ({
groups,
locale
}: {
groups: GammaGroup[];
locale: string;
}) => {
const [groupId, setGroupId] = useState<string | undefined>(undefined);
const [documentFile, setDocumentFile] = useState<File | null>(null);
const [type, setType] = useState<string>(DocumentType.MISC);
const [titleSv, setTitleSv] = useState('');
const [titleEn, setTitleEn] = useState('');
const [descriptionSv, setDescriptionSv] = useState('');
const [descriptionEn, setDescriptionEn] = useState('');

const l = i18nService.getLocale(locale);

const handleDocChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setDocumentFile(event.target.files[0]);
}
};

const handleSubmit = async (event: React.FormEvent) => {
if (groupId === undefined) return;

event.preventDefault();

const formData = new FormData();
formData.append('file', documentFile!);

await toast.promise(
addDocument(
groupId,
titleSv,
titleEn,
descriptionSv,
descriptionEn,
formData,
type as DocumentType
),
{
pending: l.docs.uploading,
success: l.docs.uploaded,
error: l.docs.uploadError
}
);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label>{l.editor.createAs} </label>
<DropdownList onChange={(e) => setGroupId(e.target.value)}>
<option value={undefined} hidden>
Select a group
</option>
{groups.map((group) => (
<option key={group.superGroup!.id} value={group.superGroup!.id}>
{group.superGroup?.prettyName ?? group.prettyName}
</option>
))}
</DropdownList>
</div>
<div>
<label>{l.docs.type} </label>
<DropdownList value={type} onChange={(e) => setType(e.target.value)}>
{Object.keys(DocumentType).map((type) => (
<option key={type} value={type}>
{
l.docTypes[
DivisionDocumentService.documentTypeKey(type as DocumentType)
]
}
</option>
))}
</DropdownList>
</div>
<label>{l.editor.title} (sv)</label>
<TextArea value={titleSv} onChange={(e) => setTitleSv(e.target.value)} />
<label>{l.editor.title} (en)</label>
<TextArea value={titleEn} onChange={(e) => setTitleEn(e.target.value)} />
<label>{l.editor.description} (sv)</label>
<TextArea
value={descriptionSv}
onChange={(e) => setDescriptionSv(e.target.value)}
/>
<label>{l.editor.description} (en)</label>
<TextArea
value={descriptionEn}
onChange={(e) => setDescriptionEn(e.target.value)}
/>
<div>
<label htmlFor="documentFile">{l.docs.file}: </label>
<input
type="file"
id="documentFile"
accept={validMimes.join(',')}
onChange={handleDocChange}
/>
</div>
<ActionButton type="submit">{l.general.upload}</ActionButton>
</form>
);
};

export default AddDocumentForm;
36 changes: 36 additions & 0 deletions src/app/[locale]/documents/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Divider from '@/components/Divider/Divider';
import AddDocumentForm from './AddDocumentForm';
import SessionService from '@/services/sessionService';
import ThreePaneLayout from '@/components/ThreePaneLayout/ThreePaneLayout';
import ContentPane from '@/components/ContentPane/ContentPane';
import i18nService from '@/services/i18nService';
import Forbidden from '@/components/ErrorPages/403/403';
import ContactCard from '@/components/ContactCard/ContactCard';

export default async function Page({
params: { locale }
}: {
params: { locale: string };
}) {
const groups = await SessionService.getActiveAddedGroups();
if (groups.length === 0) {
return <Forbidden />;
}

const l = i18nService.getLocale(locale);

return (
<main>
<ThreePaneLayout
middle={
<ContentPane>
<h1>{l.docs.createNew}</h1>
<Divider />
<AddDocumentForm groups={groups} locale={locale} />
</ContentPane>
}
right={<ContactCard locale={locale} />}
/>
</main>
);
}
22 changes: 22 additions & 0 deletions src/app/[locale]/documents/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.documentList {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
list-style-type: none;
grid-gap: 1rem;
margin-top: 1rem;
}

.title {
display: inline-block;
}

.subtitle {
font-size: var(--text-small);
margin-bottom: 0.5rem;
}

.docType {
display: inline-block;
font-size: var(--text-small);
color: #a7ebeb;
}
Loading
Loading