Skip to content

Commit 75c62f9

Browse files
authored
Merge pull request #32 from codersforcauses/issue-12-Create_Contact_Form
Issue 12 create contact form
2 parents 5535580 + 47afb13 commit 75c62f9

20 files changed

Lines changed: 620 additions & 290 deletions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useState } from "react";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { Input } from "@/components/ui/input";
5+
import { Textarea } from "@/components/ui/textarea";
6+
import api from "@/lib/api";
7+
8+
interface ContactFormData {
9+
name: string;
10+
email: string;
11+
message: string;
12+
}
13+
14+
type SubmitStatus = "idle" | "submitting" | "success" | "error";
15+
16+
const initialForm: ContactFormData = { name: "", email: "", message: "" };
17+
18+
export default function ContactForm() {
19+
const [form, setForm] = useState<ContactFormData>(initialForm);
20+
const [status, setStatus] = useState<SubmitStatus>("idle");
21+
22+
const handleChange =
23+
(field: keyof ContactFormData) =>
24+
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
25+
setForm((prev) => ({ ...prev, [field]: e.target.value }));
26+
};
27+
28+
const handleSubmit = async (e: React.FormEvent) => {
29+
e.preventDefault();
30+
setStatus("submitting");
31+
try {
32+
await api.post("/contact/contact/", form);
33+
setStatus("success");
34+
setForm(initialForm);
35+
} catch {
36+
setStatus("error");
37+
}
38+
};
39+
40+
return (
41+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
42+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
43+
<div>
44+
<label htmlFor="name" className="mb-1 block text-sm font-medium">
45+
Name
46+
</label>
47+
<Input
48+
id="name"
49+
required
50+
value={form.name}
51+
onChange={handleChange("name")}
52+
/>
53+
</div>
54+
<div>
55+
<label htmlFor="email" className="mb-1 block text-sm font-medium">
56+
Email
57+
</label>
58+
<Input
59+
id="email"
60+
type="email"
61+
required
62+
value={form.email}
63+
onChange={handleChange("email")}
64+
/>
65+
</div>
66+
</div>
67+
<div>
68+
<label htmlFor="message" className="mb-1 block text-sm font-medium">
69+
Questions or Comments
70+
</label>
71+
<Textarea
72+
id="message"
73+
required
74+
rows={8}
75+
value={form.message}
76+
onChange={handleChange("message")}
77+
/>
78+
</div>
79+
{status === "error" && (
80+
<p className="text-sm text-red-600">
81+
Something went wrong — please try again.
82+
</p>
83+
)}
84+
{status === "success" && (
85+
<p className="text-sm text-green-700">
86+
Thanks — we&apos;ll get back to you soon.
87+
</p>
88+
)}
89+
<Button
90+
type="submit"
91+
disabled={status === "submitting"}
92+
className="self-end bg-[#dd42e4] text-white hover:bg-[#dd42e4]/90"
93+
>
94+
{status === "submitting" ? "Sending..." : "Submit"}
95+
</Button>
96+
</form>
97+
);
98+
}

client/src/components/ui/input.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6+
({ className, type, ...props }, ref) => {
7+
return (
8+
<input
9+
type={type}
10+
className={cn(
11+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12+
className,
13+
)}
14+
ref={ref}
15+
{...props}
16+
/>
17+
);
18+
},
19+
);
20+
Input.displayName = "Input";
21+
22+
export { Input };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
const Textarea = React.forwardRef<
6+
HTMLTextAreaElement,
7+
React.ComponentProps<"textarea">
8+
>(({ className, ...props }, ref) => {
9+
return (
10+
<textarea
11+
className={cn(
12+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
13+
className,
14+
)}
15+
ref={ref}
16+
{...props}
17+
/>
18+
);
19+
});
20+
Textarea.displayName = "Textarea";
21+
22+
export { Textarea };

client/src/pages/contact.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Inter as FontSans } from "next/font/google";
2+
3+
import ContactForm from "@/components/contact-form";
4+
import { cn } from "@/lib/utils";
5+
import styles from "@/styles/contact.module.css";
6+
7+
const fontSans = FontSans({
8+
subsets: ["latin"],
9+
variable: "--font-sans",
10+
});
11+
12+
export default function Contact() {
13+
return (
14+
<main
15+
className={cn(
16+
"flex min-h-screen flex-col items-center gap-8 p-24 font-sans",
17+
fontSans.variable,
18+
)}
19+
>
20+
<div className={styles["hero"]}>
21+
<span className={styles["badge"]}>Contact</span>
22+
<p className={styles["subtext"]}>
23+
Have any enquiries? Have any suggestions? Ask away!
24+
</p>
25+
</div>
26+
<div className={styles["form-card"]}>
27+
<ContactForm />
28+
</div>
29+
</main>
30+
);
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.hero {
2+
width: 78vw;
3+
background-color: #fdefbd;
4+
border-radius: 1rem;
5+
padding: 2rem;
6+
}
7+
8+
.badge {
9+
display: inline-block;
10+
background-color: #fde047;
11+
color: #000;
12+
font-weight: 700;
13+
font-size: 1.5rem;
14+
padding: 0.25rem 0.75rem;
15+
border-radius: 0.25rem;
16+
}
17+
18+
.subtext {
19+
margin-top: 1rem;
20+
font-size: 1rem;
21+
color: #1f2937;
22+
}
23+
24+
.form-card {
25+
width: 78vw;
26+
max-width: 40rem;
27+
background-color: #fdefbd;
28+
border-radius: 1rem;
29+
padding: 2rem;
30+
}
31+
32+
@media (max-width: 768px) {
33+
.hero,
34+
.form-card {
35+
width: 92vw;
36+
}
37+
}

server/api/contact/__init__.py

Whitespace-only changes.

server/api/contact/admin.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.contrib import admin
2+
from .models import Contact
3+
4+
5+
class ContactAdmin(admin.ModelAdmin):
6+
list_display = ("name", "email", "message")
7+
8+
9+
# Register your models here.
10+
admin.site.register(Contact, ContactAdmin)
11+
12+
# Register your models here.

server/api/contact/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ContactConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "api.contact"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.1.14 on 2026-07-04 07:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
initial = True
8+
9+
dependencies = []
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name="Contact",
14+
fields=[
15+
(
16+
"id",
17+
models.BigAutoField(
18+
auto_created=True,
19+
primary_key=True,
20+
serialize=False,
21+
verbose_name="ID",
22+
),
23+
),
24+
("name", models.CharField(max_length=255)),
25+
("email", models.CharField(max_length=255)),
26+
("message", models.TextField()),
27+
],
28+
),
29+
]

server/api/contact/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)