Skip to content

Commit

Permalink
Overview of admissions for groups in TIHLDE (#1063)
Browse files Browse the repository at this point in the history
* first version

* redesigned
  • Loading branch information
MadsNyl authored Aug 17, 2024
1 parent 12fd0c7 commit c321877
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 28 deletions.
2 changes: 2 additions & 0 deletions src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const SignUpFeide = lazy(() => import('pages/SignUpFeide'));
const StrikeAdmin = lazy(() => import('pages/StrikeAdmin'));
const Toddel = lazy(() => import('pages/Toddel'));
const UserAdmin = lazy(() => import('pages/UserAdmin'));
const Admissions = lazy(() => import('pages/Admissions'));

type AuthRouteProps = {
/** List of permissions where the user must have access through at least one of them to be given access */
Expand Down Expand Up @@ -152,6 +153,7 @@ const AppRoutes = () => {
<Route element={<AuthRoute element={<Cheatsheet />} />} path={`${URLS.cheatsheet}*`} />
<Route element={<AuthRoute element={<ShortLinks />} />} path={URLS.shortLinks} />
<Route element={<AuthRoute element={<QRCodes />} />} path={URLS.qrCodes} />
<Route element={<AuthRoute element={<Admissions />} />} path={URLS.admissions} />

<Route element={<AuthRoute apps={[PermissionApp.BANNERS]} element={<InfoBannerAdmin />} />} path={URLS.bannerAdmin}>
<Route element={<InfoBannerAdmin />} />
Expand Down
1 change: 1 addition & 0 deletions src/URLS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const URLS = {
pythons: 'https://pythons.tihlde.org/',
pythonsLadies: 'https://pythons-damer.tihlde.org/',
changelog: '/endringslogg',
admissions: '/opptak/',
};

export default URLS;
6 changes: 3 additions & 3 deletions src/components/forms/FieldView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const FieldView = <FormValues extends FieldValues>({ formField, index, submitFor
disabled={disabled}
form={submitForm}
label={formField.title}
name={`answers.${index}.answer_text` as Path<FormValues>}
name={`answers[${index}].answer_text` as Path<FormValues>}
required={formField.required}
/>
) : formField.type === FormFieldType.MULTIPLE_SELECT ? (
Expand All @@ -30,7 +30,7 @@ const FieldView = <FormValues extends FieldValues>({ formField, index, submitFor
form={submitForm}
items={formField.options.map((option) => ({ value: option.id || '', label: option.title }))}
label={formField.title}
name={`answers.${index}.selected_options` as Path<FormValues>}
name={`answers[${index}].selected_options` as Path<FormValues>}
required={formField.required}
/>
<p className='text-muted-foreground text-sm'>Velg en eller flere svaralternativer</p>
Expand All @@ -43,7 +43,7 @@ const FieldView = <FormValues extends FieldValues>({ formField, index, submitFor
items={formField.options.map((option) => ({ value: option.id || '', label: option.title }))}
label={formField.title}
multiple={false}
name={`answers.${index}.selected_options` as Path<FormValues>}
name={`answers[${index}].selected_options` as Path<FormValues>}
required={formField.required}
/>
<p className='text-muted-foreground text-sm'>Velg ett svaralternativ</p>
Expand Down
1 change: 1 addition & 0 deletions src/components/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const NavigationContent = ({ children }: NavigationProps) => {
isAuthenticated
? {
items: [
{ title: 'Opptak', text: 'Søk verv hos TIHLDE', to: URLS.admissions },
{ title: 'Kokebok', text: 'Få hjelp til dine øvinger', to: URLS.cheatsheet },
{ title: 'Link-forkorter', text: 'Forkort linker til å peke mot TIHLDE', to: URLS.shortLinks },
{ title: 'QR koder', text: 'Generer dine egne QR koder', to: URLS.qrCodes },
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const CardHeader = forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
className={cn("flex flex-col space-y-1.5 p-4 md:p-6", className)}
{...props}
/>
))
Expand Down Expand Up @@ -59,7 +59,7 @@ const CardContent = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
<div ref={ref} className={cn("p-4 md:p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/expandable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const Expandable = ({
className={cn('whitespace-normal py-8 w-full rounded-t-md rounded-b-none bg-white dark:bg-inherit dark:hover:bg-secondary border-none flex justify-between items-center rounded-sm', expanded && 'rounded-b-none' )}
variant='outline'
>
<div className='flex items-center space-x-4'>
<div className='flex items-center space-x-2 md:space-x-4 w-full overflow-hidden'>
{ icon }
<div className='text-start break-words'>
{typeof title === 'string'
Expand All @@ -54,11 +54,11 @@ const Expandable = ({
</div>
<div className='flex items-center space-x-4'>
{ extra }
{expanded ? <ChevronDownIcon className='stroke-[1.5px]' /> : <ChevronRightIcon className='stroke-[1.5px]' />}
{expanded || open ? <ChevronDownIcon className='stroke-[1.5px]' /> : <ChevronRightIcon className='stroke-[1.5px]' />}
</div>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className='border border-t-secondary border-b-0 border-x-0 [&>*]:p-4'>
<CollapsibleContent className='border border-t-secondary border-b-0 border-x-0 [&>*]:p-2 md:[&>*]:p-4'>
{ children }
</CollapsibleContent>
</Collapsible>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ export const useDeleteGroupFine = (
});
};

export const useGroupForms = (groupSlug: string) =>
useQuery<Array<GroupForm>, RequestResponse>(GROUPS_QUERY_KEYS.forms.all(groupSlug), () => API.getGroupForms(groupSlug));
export const useGroupForms = (groupSlug: string, enabled?: boolean) =>
useQuery<Array<GroupForm>, RequestResponse>(GROUPS_QUERY_KEYS.forms.all(groupSlug), () => API.getGroupForms(groupSlug), { enabled });

export const useGroupStatistics = (groupSlug: string) =>
useQuery<GroupMemberStatistics, RequestResponse>(GROUPS_QUERY_KEYS.statistics(groupSlug), () => API.getGroupStatistics(groupSlug));
84 changes: 84 additions & 0 deletions src/pages/Admissions/components/GroupAdmission.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ArrowRight, LoaderCircle, Lock } from 'lucide-react';
import { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import URLS from 'URLS';

import { GroupList } from 'types';

import { useGroupForms } from 'hooks/Group';

import AspectRatioImg from 'components/miscellaneous/AspectRatioImg';
import { Button } from 'components/ui/button';
import Expandable from 'components/ui/expandable';
import { Skeleton } from 'components/ui/skeleton';

type GroupAdmissionProps = {
group: GroupList;
};

const GroupAdmission = ({ group }: GroupAdmissionProps) => {
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const { data: forms, isLoading } = useGroupForms(group.slug, isExpanded);

const filteredForms = useMemo(() => {
if (!forms) {
return [];
}
return forms.filter((form) => form.title.toLowerCase().includes('opptak'));
}, [forms]);

const Logo = () => (
<AspectRatioImg alt={group.image_alt || ''} className='w-[40px] h-[40px] md:w-[50px] md:h-[50px] rounded-md ratio-[1]' src={group.image || ''} />
);

return (
<Expandable className='z-10' description={group.contact_email} icon={<Logo />} onOpenChange={setIsExpanded} open={isExpanded} title={group.name}>
<div>
{isLoading && (
<div className='flex justify-center space-x-2 items-center'>
<LoaderCircle className='w-5 h-5 animate-spin' />
<p className='text-muted-foreground'>Laster inn opptaksskjema...</p>
</div>
)}
{filteredForms.length === 0 && !isLoading && <p className='text-muted-foreground text-center'>Ingen opptaksskjemaer funnet</p>}
<div className='grid grid-cols-2 gap-2'>
{filteredForms.length > 0 && (
<div>
{filteredForms[0].is_open_for_submissions ? (
<Button asChild className='w-full bg-sky-500 text-white'>
<Link to={`${URLS.form}${filteredForms[0].id}`}>
Søk nå <ArrowRight className='h-4 stroke-[1.5px]' />
</Link>
</Button>
) : (
<div className='flex items-center justify-between px-2 md:px-4 py-2 md:py-3 rounded-md border border-,muted bg-muted text-muted-foreground'>
<p>{filteredForms[0].title}</p>

<Lock className='w-4 h-4 md:w-5 md:h-5 stroke-[1.5px] text-red-500' />
</div>
)}
</div>
)}

<Button asChild variant='ghost'>
<Link to={URLS.groups.details(group.slug)}>
Les mer <ArrowRight className='h-4' />
</Link>
</Button>
</div>
</div>
</Expandable>
);
};

export const GroupAdmissionLoading = () => (
<div className='rounded-md bg-card border p-2 flex items-center space-x-4'>
<Skeleton className='w-[50px] h-[50px] rounded-full' />
<div className='space-y-2'>
<Skeleton className='w-48 h-3' />
<Skeleton className='w-32 h-2' />
</div>
</div>
);

export default GroupAdmission;
133 changes: 133 additions & 0 deletions src/pages/Admissions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { GroupList } from 'types';

import { useGroupsByType } from 'hooks/Group';
import useMediaQuery, { LARGE_SCREEN } from 'hooks/MediaQuery';

import Page from 'components/navigation/Page';

import GroupAdmission, { GroupAdmissionLoading } from './components/GroupAdmission';

const Admissions = () => {
const { BOARD_GROUPS, SUB_GROUPS, COMMITTEES, INTERESTGROUPS, isLoading } = useGroupsByType({ overview: true });

const isDesktop = useMediaQuery(LARGE_SCREEN);

type CollectionProps = {
groups: Array<GroupList>;
title: string;
};

const Collection = ({ groups, title }: CollectionProps) => (
<div className='space-y-4'>
<h1 className='text-xl font-bold'>{title}</h1>
<div className='grid md:grid-cols-2 lg:grid-cols-3 gap-4 items-baseline'>
{groups.map((group, index) => (
<GroupAdmission group={group} key={index} />
))}
</div>
</div>
);

const AdmissionInfos: AdmissionInfoProps[] = [
{
title: 'Hovedorgan',
description:
'Hovedorganet er TIHLDEs øverste organ, og består av styret og forvaltningsgruppen. Hovedstyret består av president, visepresident, økonomiminister, og ministerposter fra hver av undergruppene. Man kan ikke søke direkte til hovedstyret. Hovedstyret blir bestemt på generalforsamlingene. Forvaltningsgruppen derimot kan man søke til direkte.',
},
{
title: 'Undergrupper',
description:
'Undergruppene er TIHLDEs kjernevirksomhet. Her skjer det meste av aktiviteten. Undergruppene har egne lederverv og styrer seg selv. På grunnlag av dette er det ikke lov til å være med i mer enn én undergruppe samtidig. Selv om du kun kan være med i en undergruppe, anbefaler vi at du søker på verv i alle undergrupper du interesserer deg for.',
},
{
title: 'Komitéer',
description:
'Komitéene er TIHLDEs støtteapparat. I likhet med undergruppene utgjør de en viktig rolle i TIHLDE, men det krever litt mindre arbeid. Komitéene har også egne lederverv og styrer seg selv. Det er mulig å være med i flere komitéer samtidig, og vi anbefaler at du søker på verv i alle komitéer du interesserer deg for. Man kan også være med i en undergruppe og en komité samtidig.',
},
];

type AdmissionInfoProps = {
title: string;
description: string;
};

const AdmissionInfo = ({ title, description }: AdmissionInfoProps) => (
<div className='space-y-2'>
<h1 className='lg:text-lg font-bold text-sky-500'>{title}</h1>
<p className='text-sm lg:text-base text-slate-700 dark:text-slate-300'>{description}</p>
</div>
);

return (
<Page className='pt-0 space-y-20'>
<div className='lg:h-[50vh] relative z-0 flex items-center flex-col text-center pt-20'>
<div className='h-96 w-24 rotate-45 bg-indigo-400/40 blur-3xl absolute -bottom-24 left-1 z-0' />
<div className='h-72 lg:h-96 w-72 lg:w-96 bg-cyan-400/10 blur-3xl absolute -bottom-64 right-0 z-0' />
<div className='h-96 w-96 bg-cyan-400/30 blur-3xl absolute -top-[350px] lg:-top-[450px] right-48 z-0' />
{isDesktop && <div className='h-96 w-96 bg-cyan-400/30 blur-3xl absolute -bottom-[250px] left-1/2' />}
<h1 className='text-center text-3xl lg:text-6xl font-bold max-w-3xl mb-4'>Søk verv!</h1>
<p className='dark:text-slate-300 text-slate-700 max-w-2xl mb-8'>
Her finner du en oversikt over alle vervene i TIHLDE. Søk på vervene du er interessert i, og bli med på å skape et bedre studentmiljø!
</p>
</div>
<div className='bg-black/5 dark:bg-slate-950/30 border items-center py-12 lg:py-16 px-8 lg:px-16 rounded-3xl z-10 relative'>
<div className='space-y-12 md:space-y-20'>
<div className='space-y-4 max-w-xl w-full'>
<h1 className='text-3xl md:text-5xl font-bold'>Hva er verv?</h1>
<p className='text-sm md:text-base text-slate-700 dark:text-slate-300'>
Et verv er en oppgave eller et ansvar som du får tildelt i TIHLDE. Et verv gir deg muligheten til å være med på å skape et bedre studentmiljø, og
du lærer mye nyttig underveis som du vil ta med deg resten av livet.
</p>
</div>
<div className='grid lg:grid-cols-3 gap-12'>
{AdmissionInfos.map((info, index) => (
<AdmissionInfo key={index} {...info} />
))}
</div>
</div>
</div>

<div className='text-center flex flex-col justify-center relative z-0'>
<div className='h-96 w-24 rotate-45 bg-emerald-400/30 dark:bg-emerald-900/40 blur-3xl absolute -top-24 left-1 z-0' />
<div className='h-72 lg:h-96 w-72 lg:w-96 bg-cyan-400/10 dark:bg-cyan-900/20 blur-3xl absolute -bottom-64 right-0 z-0' />
<div className='h-96 w-96 bg-indigo-300/20 dark:bg-indigo-900/30 blur-3xl absolute top-0 right-4 z-0' />
<h2 className='text-xl md:text-5xl font-semibold max-w-5xl mx-auto'>Det er mange verv å velge mellom!</h2>
<p className='text-slate-700 dark:text-slate-300 max-w-lg mt-6 mx-auto pb-12'>
Du vil bli kalt inn til intervju etter søknadsfristen, for alle verv du har søkt på.
</p>
<div className='space-y-16'>
{isLoading && (
<div className='grid md:grid-cols-2 lg:grid-cols-3 gap-4'>
{Array.from({ length: 12 }).map((_, index) => (
<GroupAdmissionLoading key={index} />
))}
</div>
)}
{Boolean(BOARD_GROUPS.length) && <Collection groups={BOARD_GROUPS} title='Hovedorgan' />}
{Boolean(SUB_GROUPS.length) && <Collection groups={SUB_GROUPS} title='Undergrupper' />}
{Boolean(COMMITTEES.length) && <Collection groups={COMMITTEES} title='Komitéer' />}
</div>
</div>

<div className='text-center flex flex-col justify-center'>
<h2 className='text-xl md:text-5xl font-semibold max-w-5xl mx-auto'>Interessegrupper</h2>
<p className='text-slate-700 dark:text-slate-300 max-w-xl mt-6 mx-auto pb-12'>
TIHLDE har flere interessegrupper som du kan bli med i. Her kan du drive med det du er interessert i, eller lære noe nytt. Interessegruppene er åpne
for alle, og du kan være med i så mange du vil.
</p>
<div className='space-y-16'>
{isLoading && (
<div className='grid md:grid-cols-2 lg:grid-cols-3 gap-4'>
{Array.from({ length: 12 }).map((_, index) => (
<GroupAdmissionLoading key={index} />
))}
</div>
)}
{Boolean(INTERESTGROUPS.length) && <Collection groups={INTERESTGROUPS} title='Interessegrupper' />}
</div>
</div>
</Page>
);
};

export default Admissions;
26 changes: 10 additions & 16 deletions src/pages/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,16 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'compo
import { Form } from 'components/ui/form';
import { Separator } from 'components/ui/separator';

const FieldSubmissionSchema = z.object({
field: z.object({
id: z.string(),
}),
});

const TextFieldSubmissionSchema = FieldSubmissionSchema.extend({
answer_text: z.string().optional(),
});

const SelectFieldSubmissionSchema = FieldSubmissionSchema.extend({
selected_options: z.array(z.string()),
});

const formSchema = z.object({
answers: z.array(z.union([TextFieldSubmissionSchema, SelectFieldSubmissionSchema])),
answers: z.array(
z.object({
field: z.object({
id: z.string(),
}),
answer_text: z.string().optional(),
selected_options: z.array(z.string()).optional(),
}),
),
});

const FormPage = () => {
Expand Down Expand Up @@ -82,7 +76,7 @@ const FormPage = () => {
if ('selected_options' in answer) {
return {
field: { id: answer.field.id },
selected_options: answer.selected_options.map((option) => ({ id: option })),
selected_options: answer.selected_options?.map((option) => ({ id: option })) || [],
};
}

Expand Down
5 changes: 3 additions & 2 deletions src/pages/NewStudent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ArrowRight, AtSign, FacebookIcon, InstagramIcon, Users2 } from 'lucide-react';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import URLS from 'URLS';

import useMediaQuery, { LARGE_SCREEN, MEDIUM_SCREEN } from 'hooks/MediaQuery';
import { useIsAuthenticated } from 'hooks/User';
Expand Down Expand Up @@ -68,8 +69,8 @@ const NewStudent = () => {
</Button>
</a>
<Button asChild variant={'outline'}>
<Link to={isAuthenticated ? '/profil' : '/ny-bruker'}>
{isAuthenticated ? 'Se profil' : 'Opprett Bruker'} <ArrowRight className='h-4' />
<Link to={isAuthenticated ? URLS.admissions : '/ny-bruker'}>
{isAuthenticated ? 'Søk verv' : 'Opprett Bruker'} <ArrowRight className='h-4' />
</Link>
</Button>
</div>
Expand Down

0 comments on commit c321877

Please sign in to comment.