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

Mid November Update #926

Merged
merged 13 commits into from
Nov 16, 2023
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@

## Neste versjon

## versjon 2023.17.11
- ✨ **QR kode**. Du kan nå generere dine egne QR kode.
- ✨ **Aktiviteter**. Det kan nå opprettes aktiviteter for interessegrupper.

## versjon 2023.06.11
- ✨ **Arrangement**. Du kan nå se hvilken plass du har på ventelisten.
- ✨ **Arrangement**. Admins kan nå manuelt legge til deltagere på et arrangement.
- ⚡ **Kalender**. Kalender pop-up viser viser infromasjonen bedre.
- ✨ **Bannere**. Tidsbegrensede bannere kan nå opprettes.

## versjon 2023.14.10
- ✨ **QR-kode generator**. Brukere kan nå generere sine egne QR koder fra en url.

## versjon 2023.24.09

- 🎨 **Fult navn**. Kontaktperson på arrangementer vil nå bli vist med fult navn.
Expand Down
2 changes: 2 additions & 0 deletions src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const LogIn = lazy(() => import('pages/LogIn'));
const NewsAdministration = lazy(() => import('pages/NewsAdministration'));
const Wiki = lazy(() => import('pages/Wiki'));
const ShortLinks = lazy(() => import('pages/ShortLinks'));
const QRCodes = lazy(() => import('pages/QRCodes'));
const SignUp = lazy(() => import('pages/SignUp'));
const StrikeAdmin = lazy(() => import('pages/StrikeAdmin'));
const Toddel = lazy(() => import('pages/Toddel'));
Expand Down Expand Up @@ -148,6 +149,7 @@ const AppRoutes = () => {
<Route element={<AuthRoute element={<Cheatsheet />} />} path={`${URLS.cheatsheet}:studyId/:classId/`} />
<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 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 @@ -52,6 +52,7 @@ const URLS = {
profile: '/profil/',
signup: '/ny-bruker/',
shortLinks: '/linker/',
qrCodes: '/qr-koder/',
gallery: '/galleri/',
userAdmin: '/admin/brukere/',
strikeAdmin: '/admin/prikker/',
Expand Down
7 changes: 7 additions & 0 deletions src/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
PaginationResponse,
Picture,
PublicRegistration,
QRCode,
Registration,
RequestResponse,
ShortLink,
Expand Down Expand Up @@ -96,6 +97,7 @@ export const NOTIFICATIONS_ENDPOINT = 'notifications';
export const NOTIFICATION_SETTINGS_ENDPOINT = 'notification-settings';
export const WIKI_ENDPOINT = 'pages';
export const SHORT_LINKS_ENDPOINT = 'short-links';
export const QR_CODE_ENDPOINT = 'qr-codes';
export const STRIKES_ENDPOINT = 'strikes';
export const SUBMISSIONS_ENDPOINT = 'submissions';
export const USERS_ENDPOINT = 'users';
Expand Down Expand Up @@ -231,6 +233,11 @@ export default {
createShortLink: (item: ShortLink) => IFetch<ShortLink>({ method: 'POST', url: `${SHORT_LINKS_ENDPOINT}/`, data: item }),
deleteShortLink: (slug: string) => IFetch<RequestResponse>({ method: 'DELETE', url: `${SHORT_LINKS_ENDPOINT}/${slug}/` }),

// QR codes
getQRCodes: (filters?: any) => IFetch<Array<QRCode>>({ method: 'GET', url: `${QR_CODE_ENDPOINT}/`, data: filters || {} }),
createQRCode: (item: QRCode) => IFetch<QRCode>({ method: 'POST', url: `${QR_CODE_ENDPOINT}/`, data: item }),
deleteQRCode: (id: number) => IFetch<RequestResponse>({ method: 'DELETE', url: `${QR_CODE_ENDPOINT}/${String(id)}/` }),

// Gallery
getGallery: (id: Gallery['id']) => IFetch<Gallery>({ method: 'GET', url: `${GALLERY_ENDPOINT}/${id}/` }),
getGalleries: (filters?: any) => IFetch<PaginationResponse<Gallery>>({ method: 'GET', url: `${GALLERY_ENDPOINT}/`, data: filters || {} }),
Expand Down
14 changes: 12 additions & 2 deletions src/components/miscellaneous/EventListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ const EventListItem = ({ event, sx }: EventListItemProps) => {
const { observe, width } = useDimensions();
const theme = useTheme();

const getColor = () => theme.palette.colors[event.organizer?.slug.toLowerCase() === Groups.NOK.toLowerCase() ? 'nok_event' : 'other_event'];

const [height, titleFontSize, contentFontSize] = useMemo(() => {
if (width < 400) {
return [68, 18, 13];
Expand All @@ -97,6 +95,18 @@ const EventListItem = ({ event, sx }: EventListItemProps) => {
const { data: categories = [] } = useCategories();
const categoryLabel = `${event.organizer ? `${event.organizer.name} | ` : ''}${categories.find((c) => c.id === event.category)?.text || 'Laster...'}`;

const getColor = () => {
if (categories.find((c) => c.id === event.category)?.text === 'Aktivitet') {
return theme.palette.colors.activity_event;
}

if (event.organizer?.slug.toLowerCase() === Groups.NOK.toLowerCase()) {
return theme.palette.colors.nok_event;
}

return theme.palette.colors.other_event;
};

return (
<EventListItemButton
borderColor={getColor()}
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 @@ -142,6 +142,7 @@ const NavigationContent = ({ children }: NavigationProps) => {
items: [
{ text: 'Kokebok', to: URLS.cheatsheet },
{ text: 'Link-forkorter', to: URLS.shortLinks },
{ text: 'QR koder', to: URLS.qrCodes },
{ text: 'Badges ledertavler', to: URLS.badges.index },
{ text: 'Galleri', to: URLS.gallery },
],
Expand Down
29 changes: 29 additions & 0 deletions src/hooks/QRCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useMutation, UseMutationResult, useQuery, useQueryClient } from 'react-query';

import { QRCode, RequestResponse } from 'types';

import API from 'api/api';

export const QR_CODE_QUERY_KEY = 'qr-code';

export const useQRCodes = () => {
return useQuery<Array<QRCode>, RequestResponse>([QR_CODE_QUERY_KEY], () => API.getQRCodes());
};

export const useCreateQRCode = (): UseMutationResult<QRCode, RequestResponse, QRCode, unknown> => {
const queryClient = useQueryClient();
return useMutation((item) => API.createQRCode(item), {
onSuccess: () => {
queryClient.invalidateQueries(QR_CODE_QUERY_KEY);
},
});
};

export const useDeleteQRCode = (id: number): UseMutationResult<RequestResponse, RequestResponse, unknown, unknown> => {
const queryClient = useQueryClient();
return useMutation(() => API.deleteQRCode(id), {
onSuccess: () => {
queryClient.invalidateQueries(QR_CODE_QUERY_KEY);
},
});
};
151 changes: 151 additions & 0 deletions src/pages/Events/components/ActivitiesDefaultView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Button, Divider, Stack, Theme, useMediaQuery } from '@mui/material';
import { makeStyles } from 'makeStyles';
import { useCallback, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { argsToParams } from 'utils';

import { useEvents } from 'hooks/Event';
import { useIsAuthenticated } from 'hooks/User';
import { useAnalytics } from 'hooks/Utils';

import Bool from 'components/inputs/Bool';
import SubmitButton from 'components/inputs/SubmitButton';
import TextField from 'components/inputs/TextField';
import Expand from 'components/layout/Expand';
import Pagination from 'components/layout/Pagination';
import Paper from 'components/layout/Paper';
import EventListItem, { EventListItemLoading } from 'components/miscellaneous/EventListItem';
import NotFoundIndicator from 'components/miscellaneous/NotFoundIndicator';

const useStyles = makeStyles()((theme) => ({
grid: {
display: 'grid',
gridTemplateColumns: '3fr 1fr',
gridGap: theme.spacing(2),
alignItems: 'self-start',
paddingBottom: theme.spacing(2),

[theme.breakpoints.down('lg')]: {
gridTemplateColumns: '1fr',
},
},
list: {
display: 'grid',
gridTemplateColumns: '1fr',
gap: theme.spacing(1),
[theme.breakpoints.down('lg')]: {
order: 1,
},
},
settings: {
display: 'grid',
gridGap: theme.spacing(1),
position: 'sticky',
top: 80,

[theme.breakpoints.down('lg')]: {
order: 0,
position: 'static',
top: 0,
},
},
}));

type Filters = {
activity: boolean;
search?: string;
open_for_sign_up?: boolean;
user_favorite?: boolean;
expired: boolean;
};

const ActivitiesDefaultView = () => {
const isAuthenticated = useIsAuthenticated();
const { event } = useAnalytics();
const getInitialFilters = useCallback((): Filters => {
const params = new URLSearchParams(location.search);
const activity = true;
const expired = params.get('expired') ? Boolean(params.get('expired') === 'true') : false;
const open_for_sign_up = params.get('open_for_sign_up') ? Boolean(params.get('open_for_sign_up') === 'true') : undefined;
const user_favorite = params.get('user_favorite') ? Boolean(params.get('user_favorite') === 'true') : undefined;
const search = params.get('search') || undefined;
return { activity, expired, search, open_for_sign_up, user_favorite };
}, []);
const { classes } = useStyles();
const navigate = useNavigate();
const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'));
const [filters, setFilters] = useState<Filters>(getInitialFilters());
const { data, error, hasNextPage, fetchNextPage, isLoading, isFetching } = useEvents(filters);
const events = useMemo(() => (data ? data.pages.map((page) => page.results).flat() : []), [data]);
const { register, control, handleSubmit, setValue, formState } = useForm<Filters>({ defaultValues: getInitialFilters() });
const isEmpty = useMemo(() => (data !== undefined ? !data.pages.some((page) => Boolean(page.results.length)) : false), [data]);

const resetFilters = () => {
setValue('search', '');
setValue('expired', false);
setValue('user_favorite', false);
setFilters({ activity: true, expired: false, open_for_sign_up: false, user_favorite: false });
navigate(`${location.pathname}${argsToParams({ expired: false })}`, { replace: true });
};

const search = (data: Filters) => {
event('search', 'events', JSON.stringify(data));
setFilters(data);
navigate(`${location.pathname}${argsToParams(data)}`, { replace: true });
!lgDown || setSearchFormExpanded((prev) => !prev);
};

const [searchFormExpanded, setSearchFormExpanded] = useState(false);

const SearchForm = () => (
<form onSubmit={handleSubmit(search)}>
<TextField disabled={isFetching} formState={formState} label='Søk' margin='none' {...register('search')} />
<Bool control={control} formState={formState} label='Tidligere' name='expired' type='switch' />
<Bool control={control} formState={formState} label='Kun med åpen påmelding' name='open_for_sign_up' type='switch' />
{isAuthenticated && <Bool control={control} formState={formState} label='Favoritter' name='user_favorite' type='switch' />}
<SubmitButton disabled={isFetching} formState={formState}>
Søk
</SubmitButton>
<Divider sx={{ my: 1 }} />
<Button color='error' fullWidth onClick={resetFilters} variant='outlined'>
Tilbakestill
</Button>
</form>
);

return (
<>
<div className={classes.grid}>
<div className={classes.list}>
{isLoading && <EventListItemLoading />}
{isEmpty && <NotFoundIndicator header='Fant ingen arrangementer' />}
{error && <Paper>{error.detail}</Paper>}
{data !== undefined && (
<Pagination fullWidth hasNextPage={hasNextPage} isLoading={isFetching} nextPage={() => fetchNextPage()}>
<Stack gap={1}>
{events.map((event) => (
<EventListItem event={event} key={event.id} />
))}
</Stack>
</Pagination>
)}
{isFetching && <EventListItemLoading />}
</div>
{lgDown ? (
<div>
<Expand expanded={searchFormExpanded} flat header='Filtrering' onChange={() => setSearchFormExpanded((prev) => !prev)}>
<SearchForm />
</Expand>
</div>
) : (
<Paper className={classes.settings}>
<SearchForm />
</Paper>
)}
</div>
</>
);
};

export default ActivitiesDefaultView;
13 changes: 8 additions & 5 deletions src/pages/Events/components/EventsDefaultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const EventsDefaultView = () => {
setValue('category', '');
setValue('search', '');
setValue('expired', false);
setValue('user_favorite', false);
setFilters({ expired: false, open_for_sign_up: false, user_favorite: false });
navigate(`${location.pathname}${argsToParams({ expired: false })}`, { replace: true });
};
Expand All @@ -106,11 +107,13 @@ const EventsDefaultView = () => {
<TextField disabled={isFetching} formState={formState} label='Søk' margin='none' {...register('search')} />
{Boolean(categories.length) && (
<Select control={control} formState={formState} label='Kategori' name='category'>
{categories.map((value, index) => (
<MenuItem key={index} value={value.id}>
{value.text}
</MenuItem>
))}
{categories
.filter((category) => category.text !== 'Aktivitet')
.map((value, index) => (
<MenuItem key={index} value={value.id}>
{value.text}
</MenuItem>
))}
</Select>
)}
<Bool control={control} formState={formState} label='Tidligere' name='expired' type='switch' />
Expand Down
9 changes: 8 additions & 1 deletion src/pages/Events/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CelebrationIcon from '@mui/icons-material/Celebration';
import DateRange from '@mui/icons-material/DateRangeRounded';
import Reorder from '@mui/icons-material/ReorderRounded';
import { Collapse, Skeleton } from '@mui/material';
Expand All @@ -9,12 +10,15 @@ import Banner from 'components/layout/Banner';
import Tabs from 'components/layout/Tabs';
import Page from 'components/navigation/Page';

import ActivitiesDefaultView from './components/ActivitiesDefaultView';

const EventsCalendarView = lazy(() => import(/* webpackChunkName: "events_calendar" */ 'pages/Landing/components/EventsCalendarView'));

const Events = () => {
const listTab = { value: 'list', label: 'Liste', icon: Reorder };
const activityTab = { value: 'activity', label: 'Aktiviteter', icon: CelebrationIcon };
const calendarTab = { value: 'calendar', label: 'Kalender', icon: DateRange };
const tabs = [listTab, calendarTab];
const tabs = [listTab, activityTab, calendarTab];
const [tab, setTab] = useState(listTab.value);

return (
Expand All @@ -23,6 +27,9 @@ const Events = () => {
<Collapse in={tab === listTab.value}>
<EventsDefaultView />
</Collapse>
<Collapse in={tab === activityTab.value}>
<ActivitiesDefaultView />
</Collapse>
<Collapse in={tab === calendarTab.value} mountOnEnter>
<Suspense fallback={<Skeleton height={695} variant='rectangular' />}>
<EventsCalendarView />
Expand Down
Loading