Skip to content

Commit

Permalink
improve feedback system so people can provide their email and we stor…
Browse files Browse the repository at this point in the history
…e a record of it in our database

add migration for feedback table
  • Loading branch information
ob6160 committed Sep 23, 2024
1 parent 96624f5 commit adbc0f2
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 131 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.12.1
18.17.1
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@
"prisma:postgres:generate": "prisma generate",
"prisma:db:pull": "npm-run-all -p prisma:postgres:db:pull",
"prisma:postgres:db:pull": "prisma db pull",
"supabase:start": "SUPABASE_SCANNER_BUFFER_SIZE=100mb supabase start",
"supabase:start": "SUPABASE_SCANNER_BUFFER_SIZE=100mb supabase start",
"supabase:stop": "supabase stop",
"supabase:reset": "SUPABASE_SCANNER_BUFFER_SIZE=100mb supabase db reset"
"supabase:reset": "SUPABASE_SCANNER_BUFFER_SIZE=100mb supabase db reset"
},
"dependencies": {
"@apollo/client": "^3.9.10",
Expand Down Expand Up @@ -134,7 +134,7 @@
"@types/mapbox": "^1.6.45",
"@types/mapbox__geojson-area": "^0.2.2",
"@types/ngeohash": "^0.6.4",
"@types/node": "^18.16.3",
"@types/node": "^18.17.1",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"@types/styled-system": "^5.1.22",
Expand Down
178 changes: 84 additions & 94 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ model record_version {
@@schema("audit")
}

/// This model contains row level security and requires additional setup for migrations. Visit https://pris.ly/d/row-level-security for more info.
model feedback {
id Int @id @default(autoincrement())
email String? @db.VarChar(255)
feedback_text String
route String
created_at DateTime? @default(now()) @db.Timestamp(6)
@@schema("public")
}

enum operation {
INSERT
UPDATE
Expand Down
17 changes: 10 additions & 7 deletions src/api/prisma/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,22 @@ export const getLoosByProximity = async (
) => {
const nearbyLoos = (await prisma.$queryRaw`
SELECT
loo.id, loo.name, active, men, women, no_payment, notes, opening_times, payment_details,
accessible, active, all_gender, attended, automatic, location, baby_change, children, created_at,
removal_reason, radar, urinal_only, verified_at,updated_at,geohash,
loo.id, loo.name, loo.active, loo.men, loo.women, loo.no_payment, loo.notes, loo.opening_times,
loo.payment_details, loo.accessible, loo.all_gender, loo.attended, loo.automatic, loo.location,
loo.baby_change, loo.children, loo.created_at, loo.removal_reason, loo.radar, loo.urinal_only,
loo.verified_at, loo.updated_at, loo.geohash,
st_distancesphere(
geography::geometry,
loo.geography::geometry,
ST_MakePoint(${lng}, ${lat})
) as distance,
area.name as area_name,
area.type as area_type from toilets loo inner join areas area on area.id = loo.area_id
where st_distancesphere(
area.type as area_type
FROM toilets loo
INNER JOIN areas area ON area.id = loo.area_id
WHERE st_distancesphere(
loo.geography::geometry,
ST_MakePoint(${lng}, ${lat})
) <= ${radius}
) <= ${radius}
`) as (toilets & {
distance: number;
area_name?: string;
Expand Down
49 changes: 28 additions & 21 deletions src/components/Feedback/Feedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Stack } from '@mui/material';
import React, { useRef, useState } from 'react';
import Badge from '../../design-system/components/Badge';
import Button from '../../design-system/components/Button';
import Box from '../Box';
import InputField from '../../design-system/components/InputField';
import Link from 'next/link';
import TextArea from '../../design-system/components/TextArea';

enum FeedbackState {
SUCCESS = 0,
Expand All @@ -15,7 +16,6 @@ const Feedback = () => {
const [submitState, setSubmitState] = useState(FeedbackState.PENDING);

const feedbackTextArea = useRef<HTMLTextAreaElement>();
const nameInput = useRef<HTMLInputElement>();
const emailInput = useRef<HTMLInputElement>();

const submitFeedback = async () => {
Expand All @@ -25,13 +25,11 @@ const Feedback = () => {
const hasUserInputText = feedbackTextArea.current?.value.length > 0;

if (hasUserInputText) {
const input = `
Feedback :love_letter: : ${feedbackTextArea.current.value}
Name: ${nameInput.current.value ?? 'Not provided'}
Email: ${emailInput.current.value ?? 'Not provided'}
`;
const input = feedbackTextArea.current.value;
const payload = {
text: input,
email: emailInput.current.value,
route: window.location.pathname,
};

try {
Expand All @@ -46,8 +44,6 @@ Email: ${emailInput.current.value ?? 'Not provided'}
// eslint-disable-next-line functional/immutable-data
feedbackTextArea.current.value = '';
// eslint-disable-next-line functional/immutable-data
nameInput.current.value = '';
// eslint-disable-next-line functional/immutable-data
emailInput.current.value = '';

setSubmitState(FeedbackState.SUCCESS);
Expand All @@ -59,29 +55,40 @@ Email: ${emailInput.current.value ?? 'Not provided'}

return (
<Stack spacing="1rem" padding="0.5rem" width="fit-content">
<label htmlFor="nameInput">Name (optional)</label>
<InputField id="nameInput" ref={nameInput} type="text" />
{submitState === FeedbackState.SUCCESS && <Badge>Thank you!</Badge>}

<label htmlFor="emailInput">Email (optional)</label>
<label htmlFor="emailInput" style={{ fontWeight: 'bold' }}>
Email (optional)
</label>
<InputField id="emailInput" ref={emailInput} type="email" />

<label htmlFor="feedbackTextArea">Feedback</label>
<Box
as="textarea"
id="feedbackTextArea"
<label htmlFor="feedbackTextArea" style={{ fontWeight: 'bold' }}>
Feedback
</label>
<TextArea
ref={feedbackTextArea}
resize={'none'}
height="16rem"
width={['15rem', '20rem']}
id="feedbackTextArea"
style={{
resize: 'none',
height: '16rem',
width: '15rem',
}}
placeholder={`The Toilet Map is a free and open source project that we maintain in our spare time.
We'd be so grateful if you could take a moment to give us feedback on how we could make your experience even better.`}
aria-description={`The Toilet Map is a free and open source project that we maintain in our spare time.
We'd be so grateful if you could take a moment to give us feedback on how we could make your experience even better.`}
></Box>
></TextArea>

<Link
target="_blank"
href="/privacy"
style={{ fontSize: 'var(--text--1)' }}
>
Privacy Policy
</Link>

{submitState === FeedbackState.SUCCESS && <Badge>Thank you!</Badge>}
<Button
htmlElement="button"
variant="primary"
Expand Down
34 changes: 29 additions & 5 deletions src/pages/api/feedback/index.page.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../api/prisma/prisma';

async function handler(req: NextApiRequest, res: NextApiResponse) {
const feedback = req.body;
const { text, email, route } = req.body;

if (typeof text !== 'string' || text.length === 0) {
return res.status(400).send('Feedback text is required');
}

try {
// We'd like to record the full feedback entry in our database for future reference.
await prisma.feedback.create({
data: {
email: email ?? 'No email provided',
feedback_text: text,
route,
},
});

if (!URL.canParse(process.env.SLACK_FEEDBACK_WEBHOOK)) {
console.warn('Slack feedback webhook not set, skipping send to channel');
return res.send(200);
}

// We only pass on the feedback content and website path to Slack, not the users' email if they provided one.
await fetch(process.env.SLACK_FEEDBACK_WEBHOOK, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: feedback?.text }),
body: JSON.stringify({
text: `${text}\r\n------\r\nRoute: ${route}`,
}),
});
res.send(200);

return res.send(200);
} catch (error) {
console.error('There was an error sending the feedback to Slack', error);
res.send(500);
console.error('There was an error processing the feedback', error);
return res.send(500);
}
}

Expand Down
33 changes: 33 additions & 0 deletions supabase/migrations/20240823182342_feedback_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- We use this to store feedback from our users
CREATE TABLE IF NOT EXISTS public.feedback (
id SERIAL PRIMARY KEY,
email VARCHAR(255),
feedback_text TEXT NOT NULL,
route TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Grant permissions on public.feedback
ALTER TABLE IF EXISTS public.feedback OWNER TO postgres;
GRANT ALL ON TABLE public.feedback TO anon;
GRANT ALL ON TABLE public.feedback TO authenticated;
GRANT ALL ON TABLE public.feedback TO service_role;
GRANT ALL ON TABLE public.feedback TO postgres;

GRANT SELECT ON TABLE public.feedback TO toiletmap_web;
GRANT INSERT ON TABLE public.feedback TO toiletmap_web;
GRANT USAGE, SELECT ON SEQUENCE public.feedback_id_seq TO toiletmap_web;

-- Setup row-level security on public.feedback
ALTER TABLE public.feedback enable row level security;

CREATE POLICY select_policy ON public.feedback
FOR SELECT
TO toiletmap_web
USING (true);

-- Allow only inserts
CREATE POLICY insert_policy ON public.feedback
FOR INSERT
TO toiletmap_web
WITH CHECK (true); -- Allows all inserts

0 comments on commit adbc0f2

Please sign in to comment.