Skip to content

Commit a1fd258

Browse files
authored
Frontend/edwin/feedback (#121)
1 parent 1a5f69f commit a1fd258

File tree

7 files changed

+268
-20
lines changed

7 files changed

+268
-20
lines changed

frontend/package-lock.json

Lines changed: 17 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"dependencies": {
1313
"@mantine/core": "^7.17.0",
1414
"@mantine/hooks": "^7.16.1",
15-
"@mantine/notifications": "^7.17.0",
15+
"@mantine/notifications": "^7.17.1",
1616
"@next/third-parties": "^15.2.0",
1717
"@tabler/icons-react": "^3.30.0",
1818
"@tailwindcss/typography": "^0.5.16",

frontend/src/app/actions.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// frontend/src/app/actions/feedback.ts
2+
"use server";
3+
4+
// Type for our form data
5+
export interface FeedbackFormData {
6+
email?: string;
7+
message: string;
8+
}
9+
10+
export async function submitFeedback(data: FeedbackFormData) {
11+
const databaseId = process.env.NOTION_DATABASE_ID;
12+
const notionApiKey = process.env.NOTION_API_KEY;
13+
14+
if (!databaseId) {
15+
throw new Error("NOTION_DATABASE_ID environment variable is not set");
16+
}
17+
18+
if (!notionApiKey) {
19+
throw new Error("NOTION_API_KEY environment variable is not set");
20+
}
21+
22+
try {
23+
const response = await fetch("https://api.notion.com/v1/pages", {
24+
method: "POST",
25+
headers: {
26+
Authorization: `Bearer ${notionApiKey}`,
27+
"Content-Type": "application/json",
28+
"Notion-Version": "2022-06-28",
29+
},
30+
body: JSON.stringify({
31+
parent: {
32+
database_id: databaseId,
33+
},
34+
properties: {
35+
Email: {
36+
title: [
37+
{
38+
text: {
39+
content: data.email || "Anonymous",
40+
},
41+
},
42+
],
43+
},
44+
Feedback: {
45+
rich_text: [
46+
{
47+
text: {
48+
content: data.message,
49+
},
50+
},
51+
],
52+
},
53+
Date: {
54+
date: {
55+
start: new Date().toISOString(),
56+
},
57+
},
58+
},
59+
}),
60+
});
61+
62+
if (!response.ok) {
63+
const errorData = await response.json();
64+
return {
65+
success: false,
66+
message: `Notion returned an API error: ${errorData.message || response.statusText}`,
67+
};
68+
}
69+
70+
return { success: true, message: "Feedback submitted successfully." };
71+
} catch (error) {
72+
return {
73+
success: false,
74+
message: `Encountered an internal error: ${error}`,
75+
};
76+
}
77+
}

frontend/src/app/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// the tailwind class passed with className is not applied.
33
import "@mantine/core/styles.css";
44
import "./globals.css";
5+
import "@mantine/notifications/styles.css";
6+
57
import { Analytics } from "@vercel/analytics/react";
68
import { SpeedInsights } from "@vercel/speed-insights/next";
79
import { GoogleAnalytics } from "@next/third-parties/google";
@@ -16,6 +18,8 @@ import { theme } from "@/lib/theme";
1618
import { Poppins } from "next/font/google";
1719
import { FilterProvider } from "@/context/filter/filter-provider";
1820
import { Metadata } from "next";
21+
import FeedbackButton from "@/components/ui/feedback-button";
22+
import { Notifications } from "@mantine/notifications";
1923

2024
export const metadata: Metadata = {
2125
title: {
@@ -41,9 +45,11 @@ export default function RootLayout({ children }: PropsWithChildren) {
4145
<MantineProvider theme={theme} defaultColorScheme="dark">
4246
<FilterProvider>
4347
<div className="min-h-screen flex flex-col px-6">
48+
<Notifications />
4449
<NavBar />
4550
<main className="">
4651
{children}
52+
<FeedbackButton />
4753
<Analytics />
4854
<SpeedInsights />
4955
<GoogleAnalytics gaId="G-1RXLVCFJC0" />

frontend/src/components/jobs/job-pagination.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,17 @@ export default function JobPagination({ pageSize = 20 }: JobPaginationProps) {
3838
};
3939

4040
return (
41-
<div className="flex justify-center py-4">
41+
// mb-12 gives extra space for feedback button on mobile. it would've blocked the pagination controls.
42+
<div className="flex justify-center py-4 mb-12 sm:mb-0">
4243
<Pagination
4344
autoContrast
4445
value={filters.filters.page}
4546
onChange={handlePageChange}
4647
total={totalPages}
47-
siblings={1}
4848
size="md"
4949
gap={12}
50+
boundaries={1}
51+
siblings={0}
5052
radius="lg"
5153
color="accent"
5254
getItemProps={(page) => ({
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// frontend/src/components/ui/feedback-button.tsx
2+
"use client";
3+
4+
import { useState } from "react";
5+
import {
6+
Button,
7+
Modal,
8+
TextInput,
9+
Textarea,
10+
Group,
11+
ActionIcon,
12+
} from "@mantine/core";
13+
import { IconMessageCircle } from "@tabler/icons-react";
14+
import { notifications } from "@mantine/notifications";
15+
import { submitFeedback } from "@/app/actions";
16+
17+
export default function FeedbackButton() {
18+
const [opened, setOpened] = useState(false);
19+
const [email, setEmail] = useState("");
20+
const [message, setMessage] = useState("");
21+
const [isSubmitting, setIsSubmitting] = useState(false);
22+
23+
const handleSubmit = async (e: React.FormEvent) => {
24+
e.preventDefault();
25+
26+
if (
27+
!message ||
28+
!message.trim() ||
29+
message.trim().length < 3 ||
30+
message.length > 1000
31+
) {
32+
notifications.show({
33+
position: "top-center",
34+
autoClose: 1500,
35+
withCloseButton: false,
36+
color: "red",
37+
message: "Feedback must be between 3 and 1k characters!",
38+
});
39+
return;
40+
}
41+
42+
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
43+
notifications.show({
44+
position: "top-center",
45+
autoClose: 1500,
46+
withCloseButton: false,
47+
color: "red",
48+
message: "Please enter a valid email address!",
49+
});
50+
return;
51+
}
52+
setIsSubmitting(true);
53+
54+
try {
55+
// Call our server action with the form data
56+
const result = await submitFeedback({ email, message });
57+
58+
if (result.success) {
59+
// Clear form and close modal
60+
setEmail("");
61+
setMessage("");
62+
setOpened(false);
63+
64+
notifications.show({
65+
position: "top-center",
66+
autoClose: 1500,
67+
withCloseButton: false,
68+
title: "Thank you!",
69+
message: "Your feedback has been submitted.",
70+
color: "green",
71+
});
72+
} else {
73+
notifications.show({
74+
position: "top-center",
75+
title: "Error",
76+
message:
77+
result.message || "Failed to submit feedback. Please try again.",
78+
color: "red",
79+
});
80+
}
81+
} catch (error) {
82+
notifications.show({
83+
position: "top-center",
84+
title: "Error",
85+
message: "Failed to submit feedback: " + error,
86+
color: "red",
87+
});
88+
} finally {
89+
setIsSubmitting(false);
90+
}
91+
};
92+
93+
return (
94+
<>
95+
<ActionIcon
96+
className="fixed bottom-4 right-6 z-50"
97+
radius="xl"
98+
color="accent"
99+
size={50}
100+
c="black"
101+
onClick={() => setOpened(true)}
102+
>
103+
<IconMessageCircle size={20} />
104+
</ActionIcon>
105+
106+
<Modal
107+
opened={opened}
108+
onClose={() => setOpened(false)}
109+
title="Share Your Feedback"
110+
size="md"
111+
radius="lg"
112+
classNames={{
113+
content: "mt-12",
114+
}}
115+
padding="md"
116+
>
117+
<form onSubmit={handleSubmit}>
118+
<TextInput
119+
label="Email (optional)"
120+
placeholder="[email protected]"
121+
className="mb-4"
122+
value={email}
123+
onChange={(e) => setEmail(e.currentTarget.value)}
124+
type="email"
125+
/>
126+
127+
<Textarea
128+
label="Feedback"
129+
placeholder="Tell us about any bugs, features, missing listings or anything you want to say!"
130+
minRows={6}
131+
maxRows={15}
132+
className="mb-6"
133+
required
134+
autosize
135+
value={message}
136+
onChange={(e) => setMessage(e.currentTarget.value)}
137+
/>
138+
139+
<Group justify="flex-end">
140+
<Button variant="light" onClick={() => setOpened(false)}>
141+
Cancel
142+
</Button>
143+
<Button
144+
type="submit"
145+
color="accent"
146+
c="black"
147+
loading={isSubmitting}
148+
>
149+
Submit Feedback
150+
</Button>
151+
</Group>
152+
</form>
153+
</Modal>
154+
</>
155+
);
156+
}

frontend/src/context/filter/filter-provider.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ export function FilterProvider({ children }: { children: ReactNode }) {
8989
}
9090
}, [pathname, searchParams]);
9191

92+
useEffect(() => {
93+
// clear filters on return to homepage
94+
if (pathname === "/") {
95+
setFilters(emptyFilterState);
96+
}
97+
}, [pathname]);
98+
9299
// Wrapper for SelectedJob to validate attributes first
93100
const setSelectedJob = (job: Job | null) => {
94101
// Remove duplicates from working_rights

0 commit comments

Comments
 (0)