From 5a5846d853e6b482aab02aaa7ee3e08c1855ebaf Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:39:03 +0100 Subject: [PATCH] Added CSRF & YouTube and dark mode --- .env.template | 7 + backend/api/contact.go | 61 ++++ backend/api/core/csrf.go | 3 + backend/api/get_csrf.go | 19 ++ backend/go.mod | 7 + backend/go.sum | 8 + backend/main.go | 30 +- docker-compose.yaml | 2 +- frontend/app/(main)/blogs/[slug]/page.tsx | 20 +- frontend/app/(main)/page.tsx | 55 +++- frontend/app/(main)/planning/page.tsx | 199 ++++++++++++ frontend/app/globals.css | 96 +++--- frontend/app/layout.tsx | 10 +- frontend/components/ThemeProvider.tsx | 11 + frontend/components/contact.tsx | 93 +++++- frontend/components/gallery.tsx | 96 +++--- frontend/components/hero.tsx | 6 +- frontend/components/login-form.tsx | 4 +- frontend/components/nav-bar.tsx | 4 +- frontend/components/ui/calendar.tsx | 82 +++++ frontend/components/ui/dialog.tsx | 122 ++++++++ frontend/components/ui/popover.tsx | 33 ++ frontend/components/ui/switch.tsx | 46 +-- frontend/hooks/use-api.tsx | 23 +- frontend/hooks/use-login.tsx | 12 +- frontend/interfaces/youtube.ts | 48 +++ frontend/next.config.ts | 4 - frontend/package-lock.json | 356 +++++++++++++++------- frontend/package.json | 9 + 29 files changed, 1186 insertions(+), 280 deletions(-) create mode 100644 backend/api/contact.go create mode 100644 backend/api/core/csrf.go create mode 100644 backend/api/get_csrf.go create mode 100644 frontend/app/(main)/planning/page.tsx create mode 100644 frontend/components/ThemeProvider.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/interfaces/youtube.ts diff --git a/.env.template b/.env.template index ae23f5c..c44c796 100644 --- a/.env.template +++ b/.env.template @@ -15,3 +15,10 @@ FRONTEND_HOSTNAME=${FRONTEND_HOSTNAME:-latosa-frontend} BACKEND_HOSTNAME=${BACKEND_HOSTNAME:-latosa-backend} DATABASE_HOSTNAME=${DATABASE_HOSTNAME:-latosa-database} SERVER_NAME=${SERVER_NAME:-localhost} +CORS_AllowOrigin=${CORS_AllowOrigin:-*} + +SMTP_DOMAIN=${SMTP_DOMAIN:-smtp.gmail.com} +SMTP_PORT=${SMTP_PORT:-587} +SMTP_EMAIL=${SMTP_EMAIL} +SMTP_APP_PASSWORD=${SMTP_APP_PASSWORD} +YOUTUBE_API_KEY=${YOUTUBE_API_KEY} diff --git a/backend/api/contact.go b/backend/api/contact.go new file mode 100644 index 0000000..8d4333a --- /dev/null +++ b/backend/api/contact.go @@ -0,0 +1,61 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" + + "fr.latosa-escrima/api/core" + "gopkg.in/gomail.v2" +) + +type ContactForm struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + EMail string `json:"email"` + Subject string `json:"subject"` + Message string `json:"message"` +} + +func HandleContact(w http.ResponseWriter, r *http.Request) { + // TODO: Warning email not being sent ? + var form ContactForm + err := json.NewDecoder(r.Body).Decode(&form) + if err != nil { + core.JSONError{ + Status: core.Error, + Message: err.Error(), + }.Respond(w, http.StatusBadRequest) + return + } + + fmt.Println("Received form", form) + fmt.Println("ENV:", os.Environ()) + + m := gomail.NewMessage() + m.SetHeader("From", os.Getenv("SMTP_EMAIL")) + // m.SetHeader("Reply-To", form.EMail) + m.SetHeader("To", os.Getenv("SMTP_EMAIL")) + m.SetHeader("Subject", form.Subject) + m.SetBody("text/plain", fmt.Sprintf("%s %s vous a envoyé un email:\n\n%s", form.Firstname, form.Lastname, form.Message)) + port, err := strconv.Atoi(os.Getenv("SMTP_PORT")) + if err != nil { + port = 587 + } + d := gomail.NewDialer(os.Getenv("SMTP_DOMAIN"), port, os.Getenv("SMTP_EMAIL"), os.Getenv("SMTP_APP_PASSWORD")) + + if err = d.DialAndSend(); err != nil { + core.JSONError{ + Status: core.Error, + Message: err.Error(), + }.Respond(w, http.StatusInternalServerError) + return + } + + core.JSONSuccess{ + Status: core.Success, + Message: "Email sent.", + }.Respond(w, http.StatusAccepted) +} diff --git a/backend/api/core/csrf.go b/backend/api/core/csrf.go new file mode 100644 index 0000000..602bd76 --- /dev/null +++ b/backend/api/core/csrf.go @@ -0,0 +1,3 @@ +package core + +var CSRF_KEY = []byte("32-byte-long-auth-key") diff --git a/backend/api/get_csrf.go b/backend/api/get_csrf.go new file mode 100644 index 0000000..6f27b6d --- /dev/null +++ b/backend/api/get_csrf.go @@ -0,0 +1,19 @@ +package api + +import ( + "fmt" + "net/http" + + "fr.latosa-escrima/api/core" + "github.com/gorilla/csrf" +) + +func HandleCSRF(w http.ResponseWriter, r *http.Request) { + token := csrf.Token(r) + fmt.Println(token) + core.JSONSuccess{ + Status: core.Success, + Message: "CSRF generated.", + Data: map[string]string{"csrf": token}, + }.Respond(w, http.StatusOK) +} diff --git a/backend/go.mod b/backend/go.mod index 0720b0d..51ec100 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,6 +12,12 @@ require ( github.com/uptrace/bun/driver/pgdriver v1.2.8 ) +require ( + github.com/gorilla/csrf v1.7.2 // direct + github.com/gorilla/securecookie v1.1.2 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect +) + require ( github.com/fatih/color v1.18.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -24,5 +30,6 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.29.0 // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df mellium.im/sasl v0.3.2 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index b23099f..50e9fe5 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,6 +8,10 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= +github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -45,6 +49,10 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= diff --git a/backend/main.go b/backend/main.go index 615135c..01a42c5 100644 --- a/backend/main.go +++ b/backend/main.go @@ -2,29 +2,34 @@ package main import ( "fmt" - "github.com/joho/godotenv" - "github.com/uptrace/bun/extra/bundebug" "log" "net/http" "os" + "github.com/joho/godotenv" + "github.com/uptrace/bun/extra/bundebug" + _ "github.com/lib/pq" - api "fr.latosa-escrima/api" + "fr.latosa-escrima/api" "fr.latosa-escrima/api/core" + "github.com/gorilla/csrf" ) +var CORS_AllowOrigin string + func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "

Hello, World!

") } func Cors(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Allow all origins (can restrict to specific origins) - w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Origin", CORS_AllowOrigin) // Allow certain HTTP methods (you can customize these as needed) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") // Allow certain headers (you can add more as needed) - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CSRF-Token") + w.Header().Set("Access-Control-Allow-Credentials", "true") // Handle OPTIONS pre-flight request if r.Method == http.MethodOptions { return @@ -43,6 +48,7 @@ func main() { port := os.Getenv("BACKEND_DOCKER_PORT") hostname := os.Getenv("DATABASE_HOSTNAME") postgres_port := os.Getenv("POSTGRES_DOCKER_PORT") + CORS_AllowOrigin = os.Getenv("CORS_AllowOrigin") if environ == "DEV" { port = os.Getenv("BACKEND_PORT") hostname = "localhost" @@ -66,6 +72,12 @@ func main() { defer core.DB.Close() + CSRFMiddleware := csrf.Protect( + core.CSRF_KEY, + csrf.Secure(environ != "DEV"), + csrf.HttpOnly(true), + ) + mux := http.NewServeMux() core.HandleRoutes(mux, map[string]core.Handler{ @@ -108,6 +120,14 @@ func main() { Handler: api.HandleVerifyMedia, Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}, }, + "/contact": { + Handler: api.HandleContact, + Middlewares: []core.Middleware{api.Methods("POST"), CSRFMiddleware}, + }, + "/csrf-token": { + Handler: api.HandleCSRF, + Middlewares: []core.Middleware{api.Methods("GET"), CSRFMiddleware}, + }, }) fmt.Printf("Serving on port %s\n", port) diff --git a/docker-compose.yaml b/docker-compose.yaml index cbfda19..f8839af 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,7 +19,7 @@ services: depends_on: - psql volumes: - - ./backend/media:/media + - ./backend/media:/app/media build: context: ./backend/ dockerfile: Dockerfile diff --git a/frontend/app/(main)/blogs/[slug]/page.tsx b/frontend/app/(main)/blogs/[slug]/page.tsx index 2cac801..85c3a0a 100644 --- a/frontend/app/(main)/blogs/[slug]/page.tsx +++ b/frontend/app/(main)/blogs/[slug]/page.tsx @@ -9,21 +9,19 @@ export default async function HistoryDetails({ params: Promise<{ blog_id: string }>; }) { const { blog_id } = await params; - let blog = {} + let blog = {}; try { - const res = await fetch('http://localhost:3001/blogs/' + blog_id, {method: "GET"}) - blog = await res.json() - console.log(blog as Blog) - } catch(e) { + const res = await fetch("http://localhost:3001/blogs/" + blog_id, { + method: "GET", + }); + blog = await res.json(); + console.log(blog as Blog); + } catch (e) { console.log(e); } - if(blog == null) { - return( - <> - Error - - ) + if (blog == null) { + return <>Error; } const blog_item_params: BlogItemParams = { diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index 5070134..18f0266 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -4,8 +4,16 @@ import Features, { FeatureItem } from "@/components/features"; import Gallery from "@/components/gallery"; import Hero from "@/components/hero"; import Testimonial from "@/components/testimonial"; +import { CarouselItem } from "@/components/ui/carousel"; +import { IYoutube } from "@/interfaces/youtube"; export default async function Home() { + let videos: IYoutube | null = null; + if (process.env.YOUTUBE_API_KEY) { + const query = `https://www.googleapis.com/youtube/v3/search?key=${process.env.YOUTUBE_API_KEY}&channelId=UCzuFLl5I0WxSMqbeMaiq_FQ&part=snippet,id&order=date&maxResults=50`; + const res = await fetch(query); + videos = await res.json(); + } return (
@@ -20,10 +28,10 @@ export default async function Home() { position="left" image="https://shadcnblocks.com/images/block/placeholder-2.svg" > -
    +
    1. Un Système Centré sur les Concepts{" "} -
        +
        • Étude et application des meilleurs concepts et stratégies issus de @@ -37,7 +45,7 @@ export default async function Home() {
        • Éducation au Mouvement et à l’Efficacité -
            +
            • Plus qu’un enchaînement de techniques : une véritable éducation aux mouvements @@ -56,12 +64,12 @@ export default async function Home() { position="right" image="https://shadcnblocks.com/images/block/placeholder-2.svg" > -
                +
                1. Les Premières Étapes -
                    +
                    • Initialement centré sur les techniques et mouvements, le système s’est montré @@ -78,10 +86,10 @@ export default async function Home() { La Découverte des Concepts Clés {" "} -
                        +
                        • Rôle central des concepts de combat : -
                            +
                            • Puissance dans les frappes.
                            • Blocage ferme.
                            • Équilibre et attitude.
                            • @@ -103,7 +111,7 @@ export default async function Home() { > Latosa Escrima Concepts repose sur cinq concepts fondamentaux : -
                                +
                                • Équilibre
                                • Vitesse (Timing et Distance)
                                • Puissance
                                • @@ -112,8 +120,35 @@ export default async function Home() {
                                - - + + {videos && ( + + {videos.items.map((video) => { + return ( + + + + ); + })} + + )}
diff --git a/frontend/app/(main)/planning/page.tsx b/frontend/app/(main)/planning/page.tsx new file mode 100644 index 0000000..4079025 --- /dev/null +++ b/frontend/app/(main)/planning/page.tsx @@ -0,0 +1,199 @@ +"use client"; + +import "@schedule-x/theme-shadcn/dist/index.css"; +import { useNextCalendarApp, ScheduleXCalendar } from "@schedule-x/react"; +import { createEventsServicePlugin } from "@schedule-x/events-service"; +import { + CalendarEventExternal, + createViewDay, + createViewWeek, +} from "@schedule-x/calendar"; +import { useEffect, useState } from "react"; +import { format } from "date-fns"; +import { Dialog } from "@radix-ui/react-dialog"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { CalendarIcon } from "lucide-react"; +import { Calendar } from "@/components/ui/calendar"; +import { cn } from "@/lib/utils"; + +const Planning = () => { + const plugins = [createEventsServicePlugin()]; + const [eventSelected, setEventSelected] = + useState(null); + const [events, setEvents] = useState([ + { + id: "1", + title: "Event 1", + start: format(new Date(Date.now()), "yyyy-MM-dd HH:mm"), + end: format( + new Date(Date.now() + 1 * 3600 * 1000), + "yyyy-MM-dd HH:mm", + ), + }, + ]); + const calendar = useNextCalendarApp( + { + theme: "shadcn", + views: [createViewDay(), createViewWeek()], + defaultView: "week", + isDark: true, + isResponsive: true, + locale: "fr-FR", + dayBoundaries: { + start: "06:00", + end: "00:00", + }, + events, + callbacks: { + onEventClick(event, e) { + setEventSelected(event); + }, + }, + }, + plugins, + ); + + useEffect(() => { + // get all events + calendar?.events.getAll(); + }, []); + + return ( +
+
+ +
+ { + setEventSelected((e) => (open ? e : null)); + }} + > + + + {eventSelected?.title} + + {eventSelected?.description} + + + +
+
+ + {/* + + + + + + date > new Date() || + date < new Date("1900-01-01") + } + initialFocus + /> + + */} + { + const val = e.currentTarget.value; + console.log(val); + setEventSelected((ev) => { + if (ev) + return { + ...ev, + start: val, + }; + return ev; + }); + }} + className="col-span-3" + /> +
+
+ + + setEventSelected((ev) => { + if (ev) + return { + ...ev, + end: e.currentTarget.value, + }; + return ev; + }) + } + className="col-span-3" + /> +
+
+ + + +
+
+
+ ); +}; + +export default Planning; diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 296a425..f4f4112 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -8,75 +8,59 @@ body { @layer base { :root { - /* Define your custom padding value */ - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; + --background: 0 0% 98%; + --foreground: 226 32% 15%; --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; + --card-foreground: 226 32% 15%; --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; + --popover-foreground: 226 32% 15%; + --primary: 354 70% 44%; --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; + --secondary: 0 0% 94%; + --secondary-foreground: 226 32% 15%; + --muted: 0 0% 94%; + --muted-foreground: 226 32% 70%; + --accent: 354 70% 44%; + --accent-foreground: 0 0% 98%; + --destructive: 0 84% 60%; --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; + --border: 0 0% 90%; + --input: 0 0% 94%; + --ring: 354 70% 44%; + --radius: 0.75rem; + --chart-1: 354 70% 44%; + --chart-2: 20 90% 50%; + --chart-3: 200 90% 50%; + --chart-4: 300 90% 50%; + --chart-5: 60 90% 50%; } .dark { - --background: 0 0% 3.9%; + --background: 226 32% 15%; --foreground: 0 0% 98%; - --card: 0 0% 3.9%; + --card: 226 32% 20%; --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; + --popover: 226 32% 20%; --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; + --primary: 354 70% 44%; + --primary-foreground: 0 0% 98%; + --secondary: 226 32% 25%; --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; + --muted: 226 32% 25%; + --muted-foreground: 0 0% 70%; + --accent: 354 70% 44%; --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; + --destructive: 0 84% 60%; --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --border: 226 32% 35%; + --input: 226 32% 25%; + --ring: 354 70% 44%; + --radius: 0.75rem; + --chart-1: 354 70% 44%; + --chart-2: 20 90% 50%; + --chart-3: 200 90% 50%; + --chart-4: 300 90% 50%; + --chart-5: 60 90% 50%; } } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index d6ef1f6..a89e441 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "@/app/globals.css"; import SWRLayout from "@/components/layouts/swr-layout"; +import { ThemeProvider } from "@/components/ThemeProvider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -28,7 +29,14 @@ export default function RootLayout({ - {children} + + {children} + ); diff --git a/frontend/components/ThemeProvider.tsx b/frontend/components/ThemeProvider.tsx new file mode 100644 index 0000000..f6b22ae --- /dev/null +++ b/frontend/components/ThemeProvider.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/frontend/components/contact.tsx b/frontend/components/contact.tsx index 98c6c56..dfb9526 100644 --- a/frontend/components/contact.tsx +++ b/frontend/components/contact.tsx @@ -1,9 +1,71 @@ +"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; +import { ApiResponse } from "@/hooks/use-api"; +import { API_URL } from "@/lib/constants"; +import { useEffect, useState } from "react"; + +interface FormData { + firstname: string; + lastname: string; + email: string; + subject: string; + message: string; +} const Contact = () => { + const [formData, setFormData] = useState({ + firstname: "", + lastname: "", + subject: "", + email: "", + message: "", + }); + + const [csrfToken, setCsrfToken] = useState(""); + + const handleChange = ( + e: React.ChangeEvent, + ) => { + console.log(e.currentTarget); + setFormData({ + ...formData, + [e.currentTarget.name]: e.currentTarget.value, + }); + }; + + useEffect(() => { + const fetchCsrfToken = async () => { + try { + const response = await fetch(`${API_URL}/csrf-token`, { + credentials: "include", + }); + const data: ApiResponse<{ csrf: string }> = + await response.json(); + if (data.data) setCsrfToken(data.data.csrf); + } catch (e: any) { + console.log(e); + } + }; + + fetchCsrfToken(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const res = await fetch(`${API_URL}/contact`, { + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken, + }, + method: "POST", + body: JSON.stringify(formData), + credentials: "include", + }); + }; + return (
@@ -38,50 +100,75 @@ const Contact = () => {
-
+