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

feat:forms page init #186

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
23 changes: 23 additions & 0 deletions actions/form.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use server';
import { getServerAuthSession } from '~/server/auth';
import { db, forms } from '~/server/db';

export const getForms = async () => {
const session = await getServerAuthSession();
if (!session?.person) return new Error('Not authenticated');
return await db
.select({
id: forms.id,
title: forms.title,
type: forms.type,
isActive: forms.isActive,
persistentUrl: forms.persistentUrl,
expiryDate: forms.expiryDate,
})
.from(forms);
// .innerJoin(
// formsModifiableByPersons,
// eq(forms.id, formsModifiableByPersons.formId)
// )
// .where(and(eq(formsVisibleToPersons.personId, session.person.id), eq(forms.isActive, true)));
};
136 changes: 136 additions & 0 deletions app/[locale]/forms/client-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { useDebounceCallback } from 'usehooks-ts';

import {
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '~/components/inputs';
import { Button } from '~/components/ui';

import { FormsCard } from './page';

export const Types = ({ types }: { types: string[] }) => {
const searchParams = useSearchParams();
const query = searchParams.get('query') ?? '';
const currentFormType = searchParams.get('type') ?? 'all';
const formTypes = [
'all',
'academic',
'factulty feedback',
'placement',
'other',
];

return (
<>
<Select
defaultValue={currentFormType}
onValueChange={(value) =>
window.history.replaceState(null, '', `?query=${query}&type=${value}`)
}
>
<SelectTrigger className="px-4 py-5 xl:hidden">
<SelectValue placeholder="Choose a department" />
</SelectTrigger>
<SelectContent>
{formTypes.map((name, index) => (
<SelectItem key={index} value={name}>
{types[index]}
</SelectItem>
))}
</SelectContent>
</Select>
<ol className="hidden w-full space-y-4 xl:inline">
{formTypes.map((name, index) => (
<li key={index}>
<Button
active={currentFormType === name}
className="font-semibold text-shade-dark"
variant={'link'}
onClick={() =>
window.history.replaceState(
null,
'',
`?query=${query}&type=${name}`
)
}
>
{types[index]}
</Button>
</li>
))}
</ol>
</>
);
};

export const SearchInput = ({
defaultValue,
placeholder,
}: {
defaultValue?: string;
placeholder: string;
}) => {
const debounceCallback = useDebounceCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
window.history.replaceState(null, '', `?query=${event.target.value}`);
},
100
);
return (
<Input
className="!my-0 max-xl:order-first max-sm:!mb-4"
defaultValue={defaultValue}
id="forms-search"
onChange={debounceCallback}
placeholder={placeholder}
/>
);
};

export const FormsList = async ({
locale,
forms,
transText,
}: {
locale: string;
forms: {
id: number;
isActive: boolean;
type: 'academic' | 'all' | 'factulty feedback' | 'placement' | 'other';
persistentUrl: string | null;
expiryDate: Date | null;
title: string;
}[];
transText: {
active: string;
closed: string;
opened: string;
download: string;
};
}) => {
const searchParams = useSearchParams();
const query = searchParams.get('query') ?? '';
const currentFormType = searchParams.get('type') ?? 'all';
return forms
.filter(
(form) =>
(form.title.includes(query) && 1) ||
currentFormType === 'all' ||
form.type === currentFormType
)
.map((form) => (
<FormsCard
form={form}
key={form.id}
locale={locale}
transText={transText}
/>
));
};
147 changes: 140 additions & 7 deletions app/[locale]/forms/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,151 @@
import Heading from '~/components/heading';
import WorkInProgress from '~/components/work-in-progress';
import Link from 'next/link';
import { Suspense } from 'react';
import { MdOutlineFileDownload } from 'react-icons/md';

import { getForms } from '~/actions/form.actions';
import Loading from '~/components/loading';
import { Button } from '~/components/ui';
import { getTranslations } from '~/i18n/translations';
import { cn } from '~/lib/utils';

import { FormsList, SearchInput, Types } from './client-utils';

export default async function Forms({
params: { locale },
searchParams: { types: formType, query },
}: {
params: { locale: string };
searchParams: { types?: string; query?: string };
}) {
const text = (await getTranslations(locale)).Forms;

return (
<>
<Heading glyphDirection="dual" heading="h2" text={text.title} />;
<WorkInProgress locale={locale} />
</>
<section
className={cn(
'container my-6 grid gap-x-4 space-y-6 xl:gap-x-8',
'sm:grid-cols-[auto,50%] lg:grid-cols-[2fr,1fr] xl:grid-cols-[30%,auto] xl:grid-rows-[2.5rem,auto]'
)}
>
<search
className={cn(
'h-fit xl:row-span-2 xl:inline xl:rounded xl:p-4',
'xl:sticky xl:top-[88px]', // DEPENDS-ON: header.tsx
'xl:border xl:border-primary-700 xl:bg-neutral-50'
)}
>
<Types types={text.types} />
</search>

<SearchInput defaultValue={query} placeholder={text.placeholder} />

<ol className="space-y-4 max-xl:sm:col-span-2">
<Suspense fallback={<Loading />} key={`${query}-${formType}`}>
<FormsDisplay
locale={locale}
transText={{
active: text.active,
closed: text.inactive,
opened: text.opened,
download: text.download,
}}
loginPlease={{
login: text.loginPlease.login,
unauthorized: text.loginPlease.unathorised,
}}
/>
</Suspense>
</ol>
</section>
);
}

const FormsDisplay = async ({
locale,
loginPlease: { login, unauthorized },
transText,
}: {
locale: string;
loginPlease: { login: string; unauthorized: string };
transText: {
active: string;
closed: string;
opened: string;
download: string;
};
}) => {
const formsList = await getForms();
if (formsList instanceof Error)
return (
<main className="flex h-[60dvh] flex-col items-center justify-center">
<h3>{unauthorized}</h3>
<Link href={`/${locale}/login`}>
<Button className="p-1">{login}</Button>
</Link>
</main>
);
return (
<main className="grid grid-cols-2">
<FormsList forms={formsList} locale={locale} transText={transText} />
</main>
);
};

export const FormsCard = ({
form: { id, isActive, persistentUrl, expiryDate, title },
transText: { active, closed, opened, download },
locale,
}: {
form: {
id: number;
isActive: boolean;
type: 'academic' | 'all' | 'factulty feedback' | 'placement' | 'other';
persistentUrl: string | null;
expiryDate: Date | null;
title: string;
};
transText: {
active: string;
closed: string;
opened: string;
download: string;
};
locale: string;
}) => {
return (
<li
key={id}
className="block rounded-md border-l-2 border-primary-700 bg-shade-light p-4 shadow-md"
>
<Link href={`${locale}/forms/${persistentUrl ?? id}`}>
<span className="flex items-center gap-2">
<figure className="flex w-fit items-center gap-1 rounded-full bg-secondary-100/30 px-2 py-[0.05rem]">
<svg
width="101"
height="101"
viewBox="0 0 101 101"
xmlns="http://www.w3.org/2000/svg"
className="size-3 fill-secondary-500"
>
<circle cx="51" cy="51" r="49" className="opacity-30" />
<circle cx="51" cy="51" r="20" />
</svg>
<figcaption className="inline text-xs font-semibold text-secondary-300">
{isActive ? active : closed}
</figcaption>
</figure>
<p>
<span className="text-neutral-500">{opened}:</span>
{expiryDate?.toLocaleDateString(locale)}
</p>
<Button
variant="icon"
className="ms-auto !rounded-sm bg-primary-900 p-1 text-shade-light hover:bg-primary-300"
>
<MdOutlineFileDownload className="size-4" />
<p className="sr-only">{download}</p>
</Button>
</span>
<h4 className="text-shade-dark">{title}</h4>
</Link>
</li>
);
};
14 changes: 13 additions & 1 deletion i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,19 @@ const text: Translations = {
copyright:
'© 2024 National Institute of Technology Kurukshetra. All Rights Reserved.',
},
Forms: { title: 'FORMS' },
Forms: {
title: 'Forms',
placeholder: 'Search Academic, Feedback, Placements, etc...',
active: 'Active',
inactive: 'Closed',
opened: 'Opened',
download: 'Download',
loginPlease: {
unathorised: 'Not authenticated',
login: 'Login',
},
types: ['All', 'Academic', 'Factulty Feedback', 'Placement', 'Other'],
},
Header: {
institute: 'Institute',
academics: 'Academics',
Expand Down
14 changes: 13 additions & 1 deletion i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,19 @@ const text: Translations = {
copyright:
'© २०२४ राष्ट्रीय प्रौद्योगिकी संस्थान कुरूक्षेत्र। सर्वाधिकार आरक्षित।',
},
Forms: { title: 'फॉर्म्स' },
Forms: {
title: 'फॉर्म्स',
placeholder: 'शैक्षिक, प्रतिपुष्टि, नियुक्तियाँ, आदि खोजें...',
active: 'सक्रिय',
inactive: 'बंद',
opened: 'खुला',
download: 'डाउनलोड',
loginPlease: {
unathorised: 'अनधिकृत',
login: 'लॉग इन',
},
types: ['शैक्षिक', 'तथ्यपरक प्रतिपुष्टि', 'नियुक्तियाँ', 'आदि'],
},
Header: {
institute: 'संस्थान',
academics: 'शैक्षिक',
Expand Down
14 changes: 13 additions & 1 deletion i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,19 @@ export interface Translations {
lorem: string;
copyright: string;
};
Forms: { title: string };
Forms: {
title: string;
placeholder: string;
types: string[];
loginPlease: {
unathorised: string;
login: string;
};
active: string;
inactive: string;
opened: string;
download: string;
};
Header: {
institute: string;
academics: string;
Expand Down
3 changes: 3 additions & 0 deletions server/db/schema/forms.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
export const forms = pgTable('forms', {
id: serial('id').primaryKey(),
title: varchar('title').notNull(),
type: varchar('type', {
enum: ['all', 'academic', 'factulty feedback', 'placement', 'other'],
}).notNull(),
description: varchar('description').notNull(),
visibleTo: smallint('visible_to')
.array()
Expand Down
Loading