Skip to content

Commit

Permalink
feat: Improved workspace join process (#522)
Browse files Browse the repository at this point in the history
Signed-off-by: Johannes Groß <mail@gross-johannes.de>
  • Loading branch information
jo-gross authored Jan 28, 2025
1 parent d2c5440 commit 834af42
Show file tree
Hide file tree
Showing 20 changed files with 1,192 additions and 218 deletions.
1 change: 1 addition & 0 deletions .idea/prettier.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions components/modals/AddWorkspaceJoinCodeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Field, Formik, FormikProps } from 'formik';
import React, { useContext } from 'react';
import { UserContext } from '../../lib/context/UserContextProvider';
import { ModalContext } from '../../lib/context/ModalContextProvider';
import { alertService } from '../../lib/alertService';
import { useRouter } from 'next/router';

interface AddWorkspaceJoinCodeModalProps {
onCreated?: () => void;
}

export default function AddWorkspaceJoinCodeModal(props: AddWorkspaceJoinCodeModalProps) {
const userContext = useContext(UserContext);
const modalContext = useContext(ModalContext);

const router = useRouter();

const { workspaceId } = router.query;

const formRef = React.useRef<
FormikProps<{
code: string;
expires: string | undefined;
onlyUseOnce: boolean;
}>
>(null);

return (
<div className={'flex flex-col gap-2'}>
<div className={'text-2xl font-bold'}>Einladungscode hinzufügen</div>
<Formik
innerRef={formRef}
initialValues={{
code: 'abcdef',
// code: Math.random().toString(36).slice(2, 8).toLowerCase(),
expires: undefined,
onlyUseOnce: false,
}}
onSubmit={async (values) => {
try {
const body = {
code: values.code,
expires: values.expires,
onlyUseOnce: values.onlyUseOnce,
};

const response = await fetch(`/api/workspaces/${workspaceId}/join-codes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (response.status.toString().startsWith('2')) {
modalContext.closeAllModals();
props.onCreated?.();
alertService.success('Beitrittcode erstellt');
} else {
formRef.current?.setFieldValue('code', Math.random().toString(36).slice(2, 8).toLowerCase());
alertService.error('Da hat etwas nicht funktioniert, probiere es mit diesem neu generierten Code erneut!', response.status, response.statusText);
}
} catch (error) {
console.error('CocktailRatingModal -> onSubmit', error);
alertService.error('Es ist ein Fehler aufgetreten');
}
}}
validate={(values) => {
const errors: { [key: string]: string } = {};
if (values.code.length <= 5) {
errors.code = `Der Code muss länger als 5 Zeichen sein ${values.code.length}`;
}
if (values.expires && new Date(values.expires) < new Date()) {
errors.expires = 'Das Ablaufdatum muss in der Zukunft liegen';
}
return errors;
}}
>
{({ values, handleChange, handleSubmit, isSubmitting, errors, touched, handleBlur }) => (
<form onSubmit={handleSubmit} className={'flex flex-col gap-2'}>
<div className={'flex flex-col gap-2'}>
<div className={'form-control'}>
<label className={'label'} htmlFor={'code'}>
<div className={'label-text'}>
Beitrittcode <span className={'italic'}>(unveränderbar)</span>
</div>
<div className={'label-text-alt text-error'}>
<span>{errors.code && touched.code ? errors.code : ''}</span>
</div>
</label>
<input id={'code'} name={'code'} value={values.code} disabled={true} className={`input input-bordered`} />
</div>
<div className={'form-control'}>
<label className={'label'}>
<div className={'label-text'}>Ablaufdatum</div>
<div className={'label-text-alt text-error'}>
<span>{errors.expires && touched.expires ? errors.expires : ''}</span>
</div>
</label>
<input id={'expires'} name={'expires'} type={'date'} value={values.expires} onChange={handleChange} className={`input input-bordered`} />
</div>
<div className={'form-control'}>
<label className={'label'}>
<div className={'label-text'}>Einmal-Code</div>
<div className={'label-text-alt text-error'}>
<span>{errors.onlyUseOnce && touched.onlyUseOnce ? errors.onlyUseOnce : ''}</span>
</div>
</label>
<Field type={'checkbox'} name={`onlyUseOnce`} onChange={handleChange} onBlur={handleBlur} className={'toggle toggle-primary'} />
</div>
</div>
<div className={'flex justify-end gap-2'}>
<button
className={'btn btn-outline btn-error'}
type={'button'}
onClick={() => {
modalContext.closeAllModals();
}}
>
Abbrechen
</button>
<button className={'btn btn-primary'} type={'submit'}>
{isSubmitting ? <span className={'spinner loading-spinner'} /> : <></>}
Hinzufügen
</button>
</div>
</form>
)}
</Formik>
</div>
);
}
8 changes: 4 additions & 4 deletions components/modals/DeleteConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { alertService } from '../../lib/alertService';
interface DeleteConfirmationModalProps {
onApprove: () => Promise<void>;
onCancel?: () => void;
spelling: 'DELETE' | 'REMOVE';
spelling: 'DELETE' | 'REMOVE' | 'ABORT';
entityName: string;
}

Expand All @@ -16,10 +16,10 @@ export function DeleteConfirmationModal(props: DeleteConfirmationModalProps) {

return (
<div className="flex flex-col space-y-4">
<div className="text-2xl font-bold">{props.spelling == 'DELETE' ? 'Löschen' : 'Entfernen'}</div>
<div className="text-2xl font-bold">{props.spelling == 'DELETE' ? 'Löschen' : props.spelling == 'REMOVE' ? 'Entfernen' : 'Abbrechen'}</div>
<div className="max-w-xl text-justify">
Möchtest du <span className={'font-bold italic'}>{props.entityName ?? 'diesen Eintrag'}</span> wirklich{' '}
{props.spelling == 'DELETE' ? 'löschen' : 'entfernen'}?
{props.spelling == 'DELETE' ? 'löschen' : props.spelling == 'REMOVE' ? 'entfernen' : 'abbrechen'}?
</div>
<div className="flex flex-row space-x-4">
<div className={'flex-1'}></div>
Expand Down Expand Up @@ -49,7 +49,7 @@ export function DeleteConfirmationModal(props: DeleteConfirmationModalProps) {
}}
>
{isDeleting ? <span className={'loading loading-spinner'} /> : <></>}
{props.spelling == 'DELETE' ? 'Löschen' : 'Entfernen'}
{props.spelling == 'DELETE' ? 'Löschen' : props.spelling == 'REMOVE' ? 'Entfernen' : 'Abbruch bestätigen'}
</button>
</div>
</div>
Expand Down
2 changes: 0 additions & 2 deletions components/modals/SearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ export function SearchModal(props: SearchModalProps) {
fetchCocktails('');
}, [fetchCocktails, workspaceId]);

const [cocktailRatings, setCocktailRatings] = useState<Record<string, number>>();

const renderCocktailCard = (cocktail: CocktailRecipeFull, index: number, isArchived: boolean, openCard: boolean = false) => (
<div
key={'search-modal-' + cocktail.id}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"@types/node": "22.10.5",
"@types/prop-types": "15.7.13",
"@types/react": "18.3.12",
"@types/react-image-crop": "^9.0.2",
"@types/react-is": "19.0.0",
"autoprefixer": "10.4.20",
"daisyui": "4.12.23",
Expand All @@ -67,6 +66,7 @@
"eslint-config-prettier": "9.1.0",
"postcss": "8.4.49",
"prettier": "3.3.3",
"prettier-plugin-prisma": "^5.0.0",
"prettier-plugin-tailwindcss": "0.6.10",
"prisma": "6.2.1",
"semantic-release": "24.2.0",
Expand Down
17 changes: 17 additions & 0 deletions pages/api/users/workspace-requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withAuthentication } from '../../../middleware/api/authenticationMiddleware';
import { User } from '@prisma/client';
import prisma from '../../../prisma/prisma';

export default withAuthentication(async (req: NextApiRequest, res: NextApiResponse, user: User) => {
const openWorkspaceRequests = await prisma.workspaceJoinRequest.findMany({
where: {
userId: user.id,
},
include: {
workspace: true,
},
});

return res.json({ data: openWorkspaceRequests });
});
24 changes: 24 additions & 0 deletions pages/api/workspaces/[workspaceId]/join-codes/[code]/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { withHttpMethods } from '../../../../../../middleware/api/handleMethods';
import HTTPMethod from 'http-method-enum';
import { withWorkspacePermission } from '../../../../../../middleware/api/authenticationMiddleware';
import prisma from '../../../../../../prisma/prisma';
import { Role } from '@prisma/client';

export default withHttpMethods({
[HTTPMethod.DELETE]: withWorkspacePermission([Role.MANAGER], async (req, res, user, workspace) => {
try {
await prisma.workspaceJoinCode.delete({
where: {
workspaceId_code: {
workspaceId: workspace.id,
code: req.query.code as string,
},
},
});
return res.json({ data: 'ok' });
} catch (error) {
console.error(error);
return res.status(500).json({ msg: 'Error' });
}
}),
});
29 changes: 29 additions & 0 deletions pages/api/workspaces/[workspaceId]/join-codes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { withHttpMethods } from '../../../../../middleware/api/handleMethods';
import HTTPMethod from 'http-method-enum';
import { withWorkspacePermission } from '../../../../../middleware/api/authenticationMiddleware';
import prisma from '../../../../../prisma/prisma';
import { Role } from '@prisma/client';

export default withHttpMethods({
[HTTPMethod.GET]: withWorkspacePermission([Role.MANAGER], async (req, res, user, workspace) => {
const joinCodes = await prisma.workspaceJoinCode.findMany({
where: { workspaceId: workspace.id },
});

return res.json({ data: joinCodes });
}),
[HTTPMethod.POST]: withWorkspacePermission([Role.MANAGER], async (req, res, user, workspace) => {
const { code, expires, onlyUseOnce } = req.body;

const joinCodes = await prisma.workspaceJoinCode.create({
data: {
code: code,
expires: expires ? new Date(expires).toISOString() : null,
onlyUseOnce: onlyUseOnce,
workspaceId: workspace.id,
},
});

return res.json({ data: joinCodes });
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { withHttpMethods } from '../../../../../../middleware/api/handleMethods';
import HTTPMethod from 'http-method-enum';
import { withWorkspacePermission } from '../../../../../../middleware/api/authenticationMiddleware';
import prisma from '../../../../../../prisma/prisma';
import { Role } from '@prisma/client';

export default withHttpMethods({
[HTTPMethod.POST]: withWorkspacePermission([Role.MANAGER], async (req, res, user, workspace) => {
try {
await prisma.$transaction(async (transaction) => {
await transaction.workspaceJoinRequest.delete({
where: {
userId_workspaceId: {
workspaceId: workspace.id,
userId: req.query.userId as string,
},
},
});
await transaction.workspaceUser.create({
data: {
userId: req.query.userId as string,
workspaceId: workspace.id,
role: Role.USER,
},
});

//TODO: Send notification to user
});
return res.json({ data: 'ok' });
} catch (error) {
console.error(error);
return res.status(500).json({ msg: 'Error' });
}
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { withHttpMethods } from '../../../../../../middleware/api/handleMethods';
import HTTPMethod from 'http-method-enum';
import { withWorkspacePermission } from '../../../../../../middleware/api/authenticationMiddleware';
import prisma from '../../../../../../prisma/prisma';
import { Role } from '@prisma/client';

export default withHttpMethods({
[HTTPMethod.POST]: withWorkspacePermission([Role.MANAGER], async (req, res, user, workspace) => {
try {
await prisma.$transaction(async (transaction) => {
await transaction.workspaceJoinRequest.delete({
where: {
userId_workspaceId: {
workspaceId: workspace.id,
userId: req.query.userId as string,
},
},
});
//TODO: Send notification to user
});
return res.json({ data: 'ok' });
} catch (error) {
console.error(error);
return res.status(500).json({ msg: 'Error' });
}
}),
});
37 changes: 37 additions & 0 deletions pages/api/workspaces/[workspaceId]/join-requests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { withHttpMethods } from '../../../../../middleware/api/handleMethods';
import HTTPMethod from 'http-method-enum';
import { withAuthentication, withWorkspacePermission } from '../../../../../middleware/api/authenticationMiddleware';
import prisma from '../../../../../prisma/prisma';
import { Role } from '@prisma/client';

export default withHttpMethods({
[HTTPMethod.GET]: withWorkspacePermission([Role.MANAGER], async (req, res, user, workspace) => {
const joinRequests = await prisma.workspaceJoinRequest.findMany({
where: { workspaceId: workspace.id },
include: {
user: true,
},
});

return res.json({ data: joinRequests });
}),
[HTTPMethod.DELETE]: withAuthentication(async (req, res, user) => {
try {
await prisma.$transaction(async (transaction) => {
const deleteResult = await transaction.workspaceJoinRequest.delete({
where: {
userId_workspaceId: {
workspaceId: req.query.workspaceId as string,
userId: user.id as string,
},
},
});
return res.json({ data: deleteResult });
});
return res.status(500).json({ msg: 'Error' });
} catch (error) {
console.error(error);
return res.status(500).json({ msg: 'Error' });
}
}),
});
23 changes: 0 additions & 23 deletions pages/api/workspaces/[workspaceId]/join.ts

This file was deleted.

Loading

0 comments on commit 834af42

Please sign in to comment.