Skip to content

Commit

Permalink
Add feedback page
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaapple committed Dec 22, 2024
1 parent 486dff6 commit 086f1ca
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 28 deletions.
27 changes: 27 additions & 0 deletions frontend/app/[locale]/(marketing)/feedback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import FeedbackForm from '@/components/layout/feedback';
import { siteConfig } from '@/config';
import { Metadata } from 'next/types';

const seoTitle = 'MemFree AI Feedback';
const description = 'Share your Feedback to improve MemFree AI';
const url = siteConfig.url + '/feedback';

export const metadata: Metadata = {
title: seoTitle,
description: description,
alternates: {
canonical: url,
},
twitter: {
card: 'summary_large_image',
site: url,
title: seoTitle,
description: description,
images: '/og.png',
creator: '@MemFree',
},
};

export default function Feedback() {
return <FeedbackForm />;
}
53 changes: 53 additions & 0 deletions frontend/app/api/feedback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// app/api/feedback/route.ts
import { NextResponse } from 'next/server';

interface FeedbackData {
name: string;
email: string;
message: string;
type: string;
file?: string;
}

export const runtime = 'edge';

export async function POST(request: Request) {
try {
const email = process.env.FEEDBACK_EMAIL;
console.log('email', email);
const data: FeedbackData = await request.json();

const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.AUTH_RESEND_KEY}`,
},
body: JSON.stringify({
from: 'MemFree <[email protected]>',
to: email,
subject: `MemFree FeedBack - From ${data.name}`,
html: `
<h2>New FeedBack</h2>
<p><strong>Name: </strong> ${data.name}</p>
<p><strong>Email</strong> ${data.email}</p>
<p><strong>Type</strong> ${data.type}</p>
<p><strong>Image: </strong> ${data.file}</p>
<img src=${data.file}> </img>
<p><strong>Messages:</strong></p>
<p>${data.message}</p>
`,
}),
});

if (!res.ok) {
console.error('Failed to send feedback email:', await res.text());
return NextResponse.json({ error: 'failed' }, { status: 500 });
}

return NextResponse.json({ status: 200 });
} catch (error) {
console.error('Failed to send feedback email:', error);
return NextResponse.json({ error: 'failed' }, { status: 500 });
}
}
Binary file modified frontend/bun.lockb
Binary file not shown.
133 changes: 133 additions & 0 deletions frontend/components/layout/feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { SendHorizonal } from 'lucide-react';
import { toast } from 'sonner';
import { Icons } from '@/components/shared/icons';

export default function FeedbackForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [content, setContent] = useState('');
const [file, setFile] = useState('');
const [type, setType] = useState('feature');
const [loading, setLoading] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!content && !file) {
toast.error(`Please fill the content or upload a screenshot`);
return;
}
setLoading(true);
console.log('response', file);

const response = await fetch('/api/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},

body: JSON.stringify({
name: name,
email: email,
message: content,
type: type,
file: file,
}),
});

toast.success(`Thank you for your feedback!`);

setName('');
setEmail('');
setContent('');
setFile('');
setType('');
setLoading(false);
};

return (
<div className="bg-background px-4 py-10">
<div className="max-w-2xl mx-auto">
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">MemFree Feedback</h1>
<p className="text-muted-foreground mt-2">We value your feedback. Please share your thoughts with us.</p>
</div>

<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-2">
Name
</label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter your name" />
</div>

<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
Email
</label>
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter your email" />
</div>

<div>
<label htmlFor="type" className="block text-sm font-medium mb-2">
Feedback Type
</label>
<Select value={type} onValueChange={setType}>
<SelectTrigger>
<SelectValue placeholder="Select feedback type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bug">Bug Report</SelectItem>
<SelectItem value="feature">Feature Request</SelectItem>
<SelectItem value="improvement">Improvement</SelectItem>
<SelectItem value="pricing">Pricing</SelectItem>
</SelectContent>
</Select>
</div>

<div>
<label htmlFor="content" className="block text-sm font-medium mb-2">
Your Feedback
</label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Please share your feedback here..."
className="min-h-[150px]"
/>
</div>
</div>

{/* <div>
<label htmlFor="content" className="block text-sm font-medium mb-2">
Screenshot
</label>
<ImageUploader value={''} onChange={(e) => setFile(e)} showGeneratedImage={false} />
</div> */}

<Button type="submit" className="w-full md:w-auto rounded-full">
{loading ? (
<Icons.spinner size={20} strokeWidth={2} className="animate-spin" />
) : (
<div className="items-center flex space-x-2 justify-center">
<span className="font-serif text-sm">Submit Feedback</span>
<SendHorizonal className="size-4" />
</div>
)}
</Button>
</form>
</div>
</div>
</div>
);
}
6 changes: 2 additions & 4 deletions frontend/components/search/language-selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ type LanguageSelectionProps = {
};

const LanguageItem: React.FC<{ language: Language }> = ({ language }) => (
<SelectItem key={language.value} value={language.value} className="w-full p-2 block">
<div className="flex w-full justify-between">
<span className="text-md mr-2">{language.name}</span>
</div>
<SelectItem key={language.value} value={language.value}>
<span className="text-md mr-2">{language.name}</span>
</SelectItem>
);

Expand Down
10 changes: 5 additions & 5 deletions frontend/components/search/model-selection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { RowSelectItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Box } from 'lucide-react';
import { useModelStore, useUserStore } from '@/lib/store/local-store';
import { useSigninModal } from '@/hooks/use-signin-modal';
Expand Down Expand Up @@ -47,16 +47,16 @@ export const modelMap: Record<string, Model> = {
};

const ModelItem: React.FC<{ model: Model }> = ({ model }) => (
<SelectItem key={model.value} value={model.value} className="w-full p-2 block">
<div className="flex w-full justify-between">
<RowSelectItem key={model.value} value={model.value} className="w-full p-2">
<div className="flex justify-between">
<span className="text-md">{model.name}</span>
<span
className={`text-xs flex items-center justify-center ${model.flag === 'Pro' || model.flag === 'Premium' ? ' text-primary bg-purple-300 rounded-xl px-2' : ''}`}
>
{model.flag}
</span>
</div>
</SelectItem>
</RowSelectItem>
);

export function ModelSelection() {
Expand Down Expand Up @@ -101,7 +101,7 @@ export function ModelSelection() {
</div>
</SelectValue>
</SelectTrigger>
<SelectContent className="w-full">
<SelectContent>
{Object.values(modelMap).map((model) => (
<ModelItem key={model.value} model={model} />
))}
Expand Down
6 changes: 3 additions & 3 deletions frontend/components/search/source-selection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { RowSelectItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Globe } from 'lucide-react';
import { useSourceStore, useUserStore } from '@/lib/store/local-store';
import { useSigninModal } from '@/hooks/use-signin-modal';
Expand All @@ -16,14 +16,14 @@ type Source = {
};

const SourceItem: React.FC<{ source: Source }> = ({ source }) => (
<SelectItem key={source.value} value={source.value} className="w-full p-2 block">
<RowSelectItem key={source.value} value={source.value} className="w-full p-2 block">
<div className="flex w-full justify-between">
<span className="text-md mr-2">{source.name}</span>
<span className={`text-xs flex items-center justify-center ${source.flag === 'Pro' ? ' text-primary bg-purple-300 rounded-xl px-2' : ''}`}>
{source.flag}
</span>
</div>
</SelectItem>
</RowSelectItem>
);

export function SourceSelection() {
Expand Down
Loading

0 comments on commit 086f1ca

Please sign in to comment.