Skip to content

Commit 7f3c4a0

Browse files
authored
Merge pull request #93 from design-sparx/feat/updated-projects-api-url
Add project creation feature and enhance header actions
2 parents 9abdb2b + 359be3a commit 7f3c4a0

File tree

18 files changed

+660
-59
lines changed

18 files changed

+660
-59
lines changed

.changeset/dull-flies-rescue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mantine-analytics-dashboard": patch
3+
---
4+
5+
Add project creation feature and enhance header actions

app/api/auth/[...nextauth]/route.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,32 @@ export const authOptions: NextAuthOptions = {
3030

3131
const response = await res.json();
3232

33+
// If your JWT already contains the permissions
34+
// You can extract the payload portion without validating the signature
35+
// (NextAuth will handle token validation)
36+
const token = response.token;
37+
let permissions = [];
38+
39+
if (token) {
40+
try {
41+
// Split the token and decode the payload portion
42+
const payload = JSON.parse(
43+
Buffer.from(token.split('.')[1], 'base64').toString(),
44+
);
45+
permissions = payload.permission || [];
46+
} catch (e) {
47+
console.error('Error decoding JWT:', e);
48+
}
49+
}
50+
3351
// Map the backend response to the NextAuth user object
3452
const user = {
3553
id: response.user.userId,
3654
name: response.user.username,
3755
email: response.user.email,
3856
token: response.token,
3957
roles: response.roles,
58+
permissions: permissions,
4059
expiration: response.expiration,
4160
};
4261

@@ -67,6 +86,7 @@ export const authOptions: NextAuthOptions = {
6786
token.id = user.id;
6887
token.accessToken = user.token;
6988
token.roles = user.roles;
89+
token.permissions = user.permissions;
7090
token.expiration = user.expiration;
7191
}
7292
return token;
@@ -82,11 +102,13 @@ export const authOptions: NextAuthOptions = {
82102
// @ts-ignore
83103
session.roles = token.roles;
84104
// @ts-ignore
105+
session.permissions = token.permissions; // Add this line
106+
// @ts-ignore
85107
session.expiration = token.expiration;
86108
}
87109
return session;
88110
},
89-
async signOut({ token, session }) {
111+
async signOut({ token }: any) {
90112
try {
91113
// You could add server-side logout logic here if needed
92114
// For example, invalidating the token on your backend
@@ -108,8 +130,7 @@ export const authOptions: NextAuthOptions = {
108130
return true;
109131
},
110132
},
111-
secret:
112-
process.env.NEXTAUTH_SECRET || 'your-secret-key-change-this-in-production',
133+
secret: process.env.NEXTAUTH_SECRET!,
113134
};
114135

115136
const handler = NextAuth(authOptions);

app/api/projects/route.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export async function GET() {
4+
try {
5+
const response = await fetch(
6+
process.env.NEXT_PUBLIC_API_URL + '/api/projects',
7+
{
8+
headers: {
9+
'Content-Type': 'application/json',
10+
},
11+
// Add any needed credentials or headers here
12+
},
13+
);
14+
15+
const data = await response.json();
16+
return NextResponse.json(data);
17+
} catch (error) {
18+
return NextResponse.json(
19+
{ error: 'Failed to fetch projects' },
20+
{ status: 500 },
21+
);
22+
}
23+
}
24+
25+
export async function POST(request: Request) {
26+
try {
27+
const body = await request.json();
28+
29+
const response = await fetch(
30+
process.env.NEXT_PUBLIC_API_URL + '/api/projects',
31+
{
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
},
36+
body: JSON.stringify(body),
37+
},
38+
);
39+
40+
const data = await response.json();
41+
42+
if (!response.ok) {
43+
return NextResponse.json(
44+
{ error: 'Failed to create project', details: data },
45+
{ status: response.status },
46+
);
47+
}
48+
49+
return NextResponse.json(data);
50+
} catch (error) {
51+
return NextResponse.json(
52+
{ error: 'Failed to create project' },
53+
{ status: 500 },
54+
);
55+
}
56+
}

app/apps/invoices/list/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import {
44
ActionIcon,
55
Anchor,
6+
Button,
67
Container,
78
Group,
89
Paper,
910
PaperProps,
1011
Stack,
1112
Text,
1213
} from '@mantine/core';
13-
import { IconDotsVertical } from '@tabler/icons-react';
14+
import { IconDotsVertical, IconPlus } from '@tabler/icons-react';
1415

1516
import { InvoicesTable, PageHeader } from '@/components';
1617
import { useFetchData } from '@/hooks';
@@ -53,7 +54,9 @@ function Page() {
5354
<PageHeader
5455
title="Invoices"
5556
breadcrumbItems={items}
56-
invoiceAction={true}
57+
actionContent={
58+
<Button leftSection={<IconPlus size={18} />}>New Invoice</Button>
59+
}
5760
/>
5861
<Paper {...PAPER_PROPS}>
5962
<Group justify="space-between" mb="md">
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { useState } from 'react';
2+
3+
import {
4+
Button,
5+
Drawer,
6+
DrawerProps,
7+
LoadingOverlay,
8+
Stack,
9+
TextInput,
10+
Textarea,
11+
} from '@mantine/core';
12+
import { DateInput } from '@mantine/dates';
13+
import { isNotEmpty, useForm } from '@mantine/form';
14+
import { notifications } from '@mantine/notifications';
15+
16+
import { useAuth } from '@/hooks/useAuth';
17+
18+
type NewProjectDrawerProps = Omit<DrawerProps, 'title' | 'children'> & {
19+
onProjectCreated?: () => void;
20+
};
21+
22+
export const NewProjectDrawer = ({
23+
onProjectCreated,
24+
...drawerProps
25+
}: NewProjectDrawerProps) => {
26+
const { user } = useAuth();
27+
const [loading, setLoading] = useState(false);
28+
29+
const form = useForm({
30+
mode: 'controlled',
31+
initialValues: {
32+
title: '',
33+
description: '',
34+
status: 1,
35+
startDate: null,
36+
dueDate: null,
37+
},
38+
validate: {
39+
title: isNotEmpty('Project title cannot be empty'),
40+
description: isNotEmpty('Project description cannot be empty'),
41+
startDate: isNotEmpty('Start date cannot be empty'),
42+
},
43+
});
44+
45+
const handleSubmit = async (values: typeof form.values) => {
46+
setLoading(true);
47+
try {
48+
// Format dates for API
49+
const payload = {
50+
...values,
51+
startDate: values.startDate
52+
? new Date(values.startDate).toISOString()
53+
: null,
54+
dueDate: values.dueDate ? new Date(values.dueDate).toISOString() : null,
55+
ownerId: user?.id,
56+
};
57+
58+
const response = await fetch('/api/projects', {
59+
method: 'POST',
60+
headers: {
61+
'Content-Type': 'application/json',
62+
},
63+
body: JSON.stringify(payload),
64+
});
65+
66+
const data = await response.json();
67+
68+
if (!response.ok) {
69+
throw new Error(data.error || 'Failed to create project');
70+
}
71+
72+
// Show success notification
73+
notifications.show({
74+
title: 'Success',
75+
message: 'Project created successfully',
76+
color: 'green',
77+
});
78+
79+
// Reset form
80+
form.reset();
81+
82+
// Close drawer
83+
if (drawerProps.onClose) {
84+
drawerProps.onClose();
85+
}
86+
87+
// Trigger refresh of projects list
88+
if (onProjectCreated) {
89+
onProjectCreated();
90+
}
91+
} catch (error) {
92+
// Show error notification
93+
notifications.show({
94+
title: 'Error',
95+
message:
96+
error instanceof Error ? error.message : 'Failed to create project',
97+
color: 'red',
98+
});
99+
} finally {
100+
setLoading(false);
101+
}
102+
};
103+
104+
return (
105+
<Drawer {...drawerProps} title="Create a new project">
106+
<LoadingOverlay visible={loading} />
107+
<form onSubmit={form.onSubmit(handleSubmit)}>
108+
<Stack>
109+
<TextInput
110+
label="Project title"
111+
placeholder="Project title"
112+
key={form.key('title')}
113+
{...form.getInputProps('title')}
114+
required
115+
/>
116+
<Textarea
117+
label="Project description"
118+
placeholder="Project description"
119+
key={form.key('description')}
120+
{...form.getInputProps('description')}
121+
required
122+
/>
123+
<DateInput
124+
label="Start date"
125+
placeholder="Start date"
126+
key={form.key('startDate')}
127+
{...form.getInputProps('startDate')}
128+
clearable
129+
/>
130+
<DateInput
131+
label="Due date"
132+
placeholder="Due date"
133+
key={form.key('dueDate')}
134+
{...form.getInputProps('dueDate')}
135+
clearable
136+
/>
137+
<Button type="submit" mt="md" loading={loading}>
138+
Create Project
139+
</Button>
140+
</Stack>
141+
</form>
142+
</Drawer>
143+
);
144+
};
145+
146+
export default NewProjectDrawer;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.tasksCompleted {
2+
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
3+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import ProjectsCard from './ProjectsCard';
2+
3+
import type { StoryObj } from '@storybook/react';
4+
5+
6+
const MOCKS = {
7+
id: 'fe2c25c6-2cbc-4c73-9edc-8477af0000a8',
8+
title: 'Gembucket',
9+
description:
10+
'Phasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum.\n\nProin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.\n\nDuis aliquam convallis nunc. Proin at turpis a pede posuere nonummy. Integer non velit.',
11+
status: 'active',
12+
image: null,
13+
completion: 86,
14+
};
15+
16+
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
17+
const meta = {
18+
title: 'Projects/Card',
19+
component: ProjectsCard,
20+
parameters: {
21+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
22+
layout: 'centered',
23+
},
24+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
25+
tags: ['autodocs'],
26+
};
27+
28+
export default meta;
29+
type Story = StoryObj<typeof ProjectsCard>;
30+
31+
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
32+
export const Default: Story = {
33+
args: {
34+
...MOCKS,
35+
style: { width: 500 },
36+
},
37+
};

0 commit comments

Comments
 (0)