diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..78e26a1 --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,11 @@ +import { LoginForm } from "@/components/login-form" + +export default function LoginPage() { + return ( +
+
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..0db67b6 --- /dev/null +++ b/app/(auth)/signup/page.tsx @@ -0,0 +1,11 @@ +import { SignupForm } from "@/components/signup-form" + +export default function SignupPage() { + return ( +
+
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts index 370bead..69a0760 100644 --- a/app/api/auth/[...all]/route.ts +++ b/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@/lib/auth"; +import { auth } from "@/lib/auth/auth"; import { toNextJsHandler } from "better-auth/next-js"; export const { POST, GET } = toNextJsHandler(auth); \ No newline at end of file diff --git a/app/api/spotify/auth/route.ts b/app/api/spotify/auth/route.ts deleted file mode 100644 index da31d28..0000000 --- a/app/api/spotify/auth/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextResponse } from 'next/server'; - -export async function GET() { - // Redirect to Spotify authorization URL - const authURL = new URL('https://accounts.spotify.com/authorize'); - authURL.searchParams.append('client_id', process.env.SPOTIFY_CLIENT_ID!); - authURL.searchParams.append('response_type', 'code'); - authURL.searchParams.append('redirect_uri', process.env.SPOTIFY_REDIRECT_URI!); - authURL.searchParams.append('scope', 'user-read-currently-playing user-read-playback-state'); - - console.log("Authorization URL:", authURL.toString()); - return NextResponse.redirect(authURL); -} \ No newline at end of file diff --git a/app/api/spotify/callback/route.ts b/app/api/spotify/callback/route.ts deleted file mode 100644 index 06fa7b6..0000000 --- a/app/api/spotify/callback/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function GET(request: Request) { - const { searchParams, href } = new URL(request.url); - console.log("Callback URL:", href); // Debug - const code = searchParams.get("code"); - if (!code) { - return NextResponse.json({ error: "No code provided" }, { status: 400 }); - } - - try { - const response = await fetch("https://accounts.spotify.com/api/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Basic ${Buffer.from( - `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}` - ).toString("base64")}`, - }, - body: new URLSearchParams({ - grant_type: "authorization_code", - code, - redirect_uri: process.env.SPOTIFY_REDIRECT_URI!, - }), - }); - - const data = await response.json(); - if (!response.ok) { - throw new Error(data.error || "Failed to exchange code"); - } - - const { refresh_token } = data; - console.log("Refresh Token:", refresh_token); - - return NextResponse.json({ - message: "Copy the refresh_token to your .env.local", - refresh_token, - }); - } catch (error) { - console.error("Token exchange error:", error); - return NextResponse.json( - { error: "Failed to get tokens" }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/spotify/route.ts b/app/api/spotify/route.ts index 469cf59..f64f17a 100644 --- a/app/api/spotify/route.ts +++ b/app/api/spotify/route.ts @@ -1,5 +1,15 @@ import { NextResponse } from "next/server"; +/** + * Spotify "Now Playing" API + * + * Setup (one-time): + * 1. Go to https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&scope=user-read-currently-playing%20user-read-playback-state + * 2. Authorize and copy the `code` from the redirect URL + * 3. Exchange it for tokens via POST to https://accounts.spotify.com/api/token + * 4. Save the refresh_token to SPOTIFY_REFRESH_TOKEN in .env.local + */ + // In-memory store for access token and expiration let accessToken: string | null = null; let tokenExpiration: number | null = null; diff --git a/components/footer/Footer.tsx b/components/footer/Footer.tsx index 754688e..a016d31 100644 --- a/components/footer/Footer.tsx +++ b/components/footer/Footer.tsx @@ -1,31 +1,39 @@ import Image from "next/image"; -import SpotifyStatus from './SpotifyStatus'; -import SocialMediaLinks from './SocialMediaLinks'; -import Copyright from './Copyright'; +import SpotifyStatus from "./SpotifyStatus"; +import SocialMediaLinks from "./SocialMediaLinks"; +import Copyright from "./Copyright"; +import { ASSETS } from "@/lib/constants"; export default function Footer() { - const baseUrl = "/svgs/"; - return ( ); -} \ No newline at end of file +} diff --git a/components/footer/SocialMediaLinks.tsx b/components/footer/SocialMediaLinks.tsx index 5ae28d5..15d4e4e 100644 --- a/components/footer/SocialMediaLinks.tsx +++ b/components/footer/SocialMediaLinks.tsx @@ -1,21 +1,26 @@ -import Image from 'next/image'; -import { socialMedia } from '@/lib/socialMedia'; - - -export interface SocialMediaItem { - name: string; - url: string; - icon: string; -} +import Image from "next/image"; +import { socialMedia } from "@/lib/socialMedia"; export default function SocialMediaLinks() { return (
{socialMedia.map((media) => ( - - {`${media.name} + + {`${media.name} ))}
); -} \ No newline at end of file +} diff --git a/components/footer/SpotifyStatus.tsx b/components/footer/SpotifyStatus.tsx index dc8df60..14d059d 100644 --- a/components/footer/SpotifyStatus.tsx +++ b/components/footer/SpotifyStatus.tsx @@ -1,26 +1,19 @@ -'use client'; -import { useEffect, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +"use client"; +import { useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import type { SpotifyStatus } from "@/lib/types"; -interface SpotifyStatus { - isListening: boolean; - trackName?: string; - artistName?: string; - message?: string; - itemType?: string; -} - -export default function SpotifyStatus() { +export default function SpotifyStatusComponent() { const [status, setStatus] = useState({ isListening: false }); useEffect(() => { async function fetchSpotifyStatus() { try { - const response = await fetch('api/spotify'); + const response = await fetch("api/spotify"); const data: SpotifyStatus = await response.json(); setStatus(data); } catch { - setStatus({ isListening: false, message: 'Error fetching Spotify data' }); + setStatus({ isListening: false, message: "Error fetching Spotify data" }); } } fetchSpotifyStatus(); @@ -30,10 +23,10 @@ export default function SpotifyStatus() { const formatNowPlaying = () => { if (!status.isListening) { - return status.message || 'Not listening to Spotify'; + return status.message || "Not listening to Spotify"; } - if (status.itemType === 'episode') { + if (status.itemType === "episode") { return `Now Playing: ${status.trackName} from ${status.artistName}`; } else { return `Now Playing: ${status.trackName} by ${status.artistName}`; @@ -41,12 +34,16 @@ export default function SpotifyStatus() { }; return ( - <> - - - {formatNowPlaying()} - - - + + + {formatNowPlaying()} + + ); -} \ No newline at end of file +} diff --git a/components/header/MobileMenuToggle.tsx b/components/header/MobileMenuToggle.tsx new file mode 100644 index 0000000..2b11433 --- /dev/null +++ b/components/header/MobileMenuToggle.tsx @@ -0,0 +1,37 @@ +"use client"; +import { useState } from "react"; +import NavbarLinks from "./NavbarLinks"; + +export default function MobileMenuToggle() { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + {isOpen && ( +
+
+ setIsOpen(false)} + /> +
+
+ )} + + ); +} diff --git a/components/header/Navbar.tsx b/components/header/Navbar.tsx index d8f9ca4..f9b9eeb 100644 --- a/components/header/Navbar.tsx +++ b/components/header/Navbar.tsx @@ -1,85 +1,29 @@ -'use client'; -import { useState, useEffect } from 'react'; -import Link from 'next/link'; -import NavbarLinks from './NavbarLinks'; -import classes from './navbar.module.css'; -import { Avatar, AvatarImage } from '@/components/ui/avatar'; -import { Skeleton } from '@/components/ui/skeleton'; -import { ModeToggle } from '@/components/ui/mode-toggle'; -import { ProfileToggle } from '@/components/ui/user'; +import Link from "next/link"; +import NavbarLinks from "./NavbarLinks"; +import MobileMenuToggle from "./MobileMenuToggle"; +import { Avatar, AvatarImage } from "@/components/ui/avatar"; +import { ModeToggle } from "@/components/ui/mode-toggle"; +import { ProfileToggle } from "@/components/ui/user"; +import { ASSETS } from "@/lib/constants"; export default function Navbar() { - const imageSrc = "/images/Logo.jpeg"; - const [isOpen, setIsOpen] = useState(false); - const [isClosing, setIsClosing] = useState(false); - const [isImageLoaded, setIsImageLoaded] = useState(false); - const [scrolled, setScrolled] = useState(false); + return ( +
+ +
+ ); +} diff --git a/components/header/NavbarLinks.tsx b/components/header/NavbarLinks.tsx index 7d72ff6..d7b191f 100644 --- a/components/header/NavbarLinks.tsx +++ b/components/header/NavbarLinks.tsx @@ -1,54 +1,26 @@ -'use client'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; - -interface NavLinkItem { - name: string; - href: string; - target?: string; - rel?: string; - isContactLink?: boolean; -} - -const navLinks: NavLinkItem[] = [ - { name: 'Placeholder 1', href: '#' }, - { name: 'Placeholder 2', href: '#' }, - { name: 'Placeholder 3', href: '#' }, - { name: 'Contact', href: '/#contact-section', isContactLink: true }, -]; +"use client"; +import Link from "next/link"; +import { NAV_LINKS } from "@/lib/constants"; interface NavbarLinksProps { - className?: string; - onLinkClick?: () => void; + className?: string; + onLinkClick?: () => void; } export default function NavbarLinks({ className, onLinkClick }: NavbarLinksProps) { - const pathname = usePathname(); - - const handleLinkClick = (isContactLink?: boolean, event?: React.MouseEvent) => { - if (onLinkClick) { - onLinkClick(); - } - - if (isContactLink && pathname === '/') { - if (event) event.preventDefault(); - const contactSection = document.getElementById('contact-section'); - if (contactSection) { - contactSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - }; - - return ( - - ); -} \ No newline at end of file + return ( + + ); +} diff --git a/components/header/navbar.module.css b/components/header/navbar.module.css deleted file mode 100644 index c6245b7..0000000 --- a/components/header/navbar.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.animate-slide-down { - animation: slide-down 0.7s cubic-bezier(0.4, 0, 0.2, 1); -} - -.animate-slide-up { - animation: slide-up 0.7s cubic-bezier(0.4, 0, 0.2, 1) forwards; -} - -@keyframes slide-down { - from { - transform: translateY(-20px); - max-height: 0; - opacity: 0; - } - to { - transform: translateY(0); - max-height: 600px; - opacity: 1; - } -} - -@keyframes slide-up { - from { - opacity: 1; - transform: translateY(0); - max-height: 600px; - } - to { - opacity: 0; - transform: translateY(-20px); - max-height: 0; - } -} \ No newline at end of file diff --git a/components/login-form.tsx b/components/login-form.tsx new file mode 100644 index 0000000..5c96a22 --- /dev/null +++ b/components/login-form.tsx @@ -0,0 +1,92 @@ +"use client" +import Image from "next/image" +import Link from "next/link" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { toast } from "sonner" +import { useActionState, useEffect } from "react" +import { signIn } from "@/lib/actions" +import { loginWithSocial } from "@/lib/auth/auth-client" + +export function LoginForm({ className, ...props }: React.ComponentProps<"div">) { + const initialState = { errorMessage: "" }; + const [state, formAction, pending] = useActionState(signIn, initialState) + + useEffect(() => { + if (state.errorMessage && state.errorMessage.length) { + toast.error(state.errorMessage) + } + }, [state.errorMessage]) + + return ( +
+ + +
+
+
+

Welcome back

+

+ Login to your BattlePlan account +

+
+
+ + +
+
+
+ + + Forgot your password? + +
+ +
+ +
+ + Or continue with + +
+
+ + + +
+
+ Don't have an account?{" "} + + Sign up + +
+
+
+
+ BattlePlan Logo +
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/sections/contact/ContactForm.tsx b/components/sections/contact/ContactForm.tsx index 239593c..b8c0027 100644 --- a/components/sections/contact/ContactForm.tsx +++ b/components/sections/contact/ContactForm.tsx @@ -1,27 +1,26 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; -import { z } from "zod"; import { useState } from "react"; -import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; -import { toast } from "sonner" - - -const formSchema = z.object({ - name: z.string().min(2, "Name must be at least 2 characters long"), - email: z.string().email("Invalid email address"), - message: z.string().min(10, "Message must be at least 10 characters long"), -}); +import { toast } from "sonner"; +import { contactFormSchema, type ContactFormData } from "@/lib/schemas/contact"; export function ContactForm() { const [isSubmitting, setIsSubmitting] = useState(false); - // Define the form schema using Zod - const form = useForm>({ - resolver: zodResolver(formSchema), + const form = useForm({ + resolver: zodResolver(contactFormSchema), defaultValues: { name: "", email: "", @@ -29,15 +28,14 @@ export function ContactForm() { }, }); - // Handle form submission - async function onSubmit(data: z.infer) { + async function onSubmit(data: ContactFormData) { setIsSubmitting(true); try { - const response = await fetch('/api/email', { - method: 'POST', + const response = await fetch("/api/email", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(data), }); @@ -45,21 +43,21 @@ export function ContactForm() { const result = await response.json(); if (response.ok) { - toast('Message sent successfully! I\'ll get back to you soon.', { - position: 'bottom-right', + toast("Message sent successfully! I'll get back to you soon.", { + position: "bottom-right", duration: 5000, }); form.reset(); } else { - toast(result.error || 'Failed to send message. Please try again.', { - position: 'bottom-right', + toast(result.error || "Failed to send message. Please try again.", { + position: "bottom-right", duration: 5000, }); } } catch (error) { - console.error('Form submission error:', error); - toast('Network error. Please check your connection and try again.', { - position: 'top-right', + console.error("Form submission error:", error); + toast("Network error. Please check your connection and try again.", { + position: "top-right", duration: 5000, }); } finally { @@ -128,12 +126,15 @@ export function ContactForm() { /> - {/* Submit Button */} - ); -} \ No newline at end of file +} diff --git a/components/signup-form.tsx b/components/signup-form.tsx new file mode 100644 index 0000000..2db1e96 --- /dev/null +++ b/components/signup-form.tsx @@ -0,0 +1,92 @@ +"use client" +import Image from "next/image" +import Link from "next/link" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { signUp } from "@/lib/actions" +import { useActionState, useEffect } from "react" +import { toast } from "sonner" + +export function SignupForm({ className, ...props }: React.ComponentProps<"div">) { + const initialState = { errorMessage: "" }; + const [state, formAction, pending] = useActionState(signUp, initialState) + + useEffect(() => { + if (state.errorMessage && state.errorMessage.length) { + toast.error(state.errorMessage) + } + }, [state.errorMessage]) + + return ( +
+ + +
+
+
+

Welcome!

+

+ Create your BattlePlan account +

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+ +
+ +
+ + Or continue with + +
+
+ + + +
+
+ Already have an account?{" "} + + Sign in + +
+
+
+
+ BattlePlan Logo +
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/mode-toggle.tsx b/components/ui/mode-toggle.tsx index ee7c498..d888f5d 100644 --- a/components/ui/mode-toggle.tsx +++ b/components/ui/mode-toggle.tsx @@ -22,7 +22,7 @@ export function ModeToggle() { Toggle theme - + setTheme("light")}> Light diff --git a/components/ui/user.tsx b/components/ui/user.tsx index c9bfabc..67efbac 100644 --- a/components/ui/user.tsx +++ b/components/ui/user.tsx @@ -3,7 +3,7 @@ import Link from "next/link" import { UserRound } from "lucide-react" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { authClient } from "@/lib/auth-client" +import { authClient } from "@/lib/auth/auth-client" import { useRouter } from "next/navigation" import { toast } from "sonner" diff --git a/lib/actions.ts b/lib/actions.ts new file mode 100644 index 0000000..0e86858 --- /dev/null +++ b/lib/actions.ts @@ -0,0 +1,81 @@ +"use server" +import { redirect } from "next/navigation"; +import { auth } from "./auth/auth"; +import { APIError } from "better-auth/api"; + +interface State { + errorMessage?: string | null; +} + +export async function signUp(prevState: State, formData: FormData) { + const rawFormData = { + firstName: formData.get("firstName") as string, + lastName: formData.get("lastName") as string, + email: formData.get("email") as string, + password: formData.get("password") as string, + }; + + const { firstName, lastName, email, password } = rawFormData; + + try { + await auth.api.signUpEmail({ + body: { + name: `${firstName} ${lastName}`, + email, + password, + }, + }); + } catch (error) { + if (error instanceof APIError) { + switch (error.status) { + case "UNPROCESSABLE_ENTITY": + return { errorMessage: "User already exists." }; + case "BAD_REQUEST": + return { errorMessage: "Invalid email." }; + default: + return { errorMessage: "Something went wrong." }; + } + } + console.error(error); + } + + redirect("/"); +} + +export async function signIn(prevState: State, formData: FormData) { + const rawFormData = { + email: formData.get("email") as string, + password: formData.get("password") as string, + }; + + const { email, password } = rawFormData; + + try { + await auth.api.signInEmail({ + body: { + email, + password, + }, + }); + } catch (error) { + if (error instanceof APIError) { + console.log(error); + switch (error.status) { + case "UNAUTHORIZED": + return { errorMessage: "Invalid credentials." }; + case "BAD_REQUEST": + return { errorMessage: "Invalid email." }; + default: + return { errorMessage: "Something went wrong." }; + } + } + console.error("Sign in with email and password has not worked.", error); + } + + redirect("/"); +} + +export async function signOut() { + await auth.api.signOut(); + redirect("/auth/login"); +} \ No newline at end of file diff --git a/lib/auth-client.ts b/lib/auth-client.ts deleted file mode 100644 index bef41cd..0000000 --- a/lib/auth-client.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createAuthClient } from "better-auth/react" -import { stripeClient } from "@better-auth/stripe/client" - -export const authClient = createAuthClient({ - baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000", - plugins: [ - stripeClient({ - subscription: true - }) - ] -}) \ No newline at end of file diff --git a/lib/auth/auth-client.ts b/lib/auth/auth-client.ts new file mode 100644 index 0000000..acf50ac --- /dev/null +++ b/lib/auth/auth-client.ts @@ -0,0 +1,28 @@ +import { createAuthClient } from "better-auth/react" +import { stripeClient } from "@better-auth/stripe/client" +import { APIError } from "better-auth" + +export const authClient = createAuthClient({ + baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000", + plugins: [ + stripeClient({ + subscription: true + }) + ] +}) + +export const loginWithSocial = async (provider: string) => { + try { + await authClient.signIn.social({ + provider, + callbackURL: "/", + errorCallbackURL: "/login", + }) + } catch (error) { + if (error instanceof APIError) { + console.error("Social login error:", error.status, error.message) + } else { + console.error("Unexpected error during social login:", error) + } + } +} \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth/auth.ts similarity index 95% rename from lib/auth.ts rename to lib/auth/auth.ts index 3a5a353..3305aa4 100644 --- a/lib/auth.ts +++ b/lib/auth/auth.ts @@ -1,8 +1,9 @@ import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; -import { prisma } from "./prisma"; +import { prisma } from "../prisma"; import { stripe } from "@better-auth/stripe"; import Stripe from "stripe"; +import { nextCookies } from "better-auth/next-js"; // Initialize Stripe integration const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, { @@ -77,6 +78,6 @@ export const auth = betterAuth({ return isOwner; } } - }) - ] + }), + nextCookies()], }); \ No newline at end of file diff --git a/lib/constants/assets.ts b/lib/constants/assets.ts new file mode 100644 index 0000000..9c86676 --- /dev/null +++ b/lib/constants/assets.ts @@ -0,0 +1,6 @@ +export const ASSETS = { + svgBasePath: "/svgs/", + imagesBasePath: "/images/", + logo: "/images/Logo.jpeg", + spotifyIcon: "/svgs/Spotify.svg", +} as const; diff --git a/lib/constants/index.ts b/lib/constants/index.ts new file mode 100644 index 0000000..babc2e6 --- /dev/null +++ b/lib/constants/index.ts @@ -0,0 +1,3 @@ +export * from "./assets"; +export * from "./urls"; +export * from "./navigation"; diff --git a/lib/constants/navigation.ts b/lib/constants/navigation.ts new file mode 100644 index 0000000..a467e75 --- /dev/null +++ b/lib/constants/navigation.ts @@ -0,0 +1,8 @@ +import type { NavLinkItem } from "@/lib/types"; + +export const NAV_LINKS: NavLinkItem[] = [ + { name: "Placeholder 1", href: "#" }, + { name: "Placeholder 2", href: "#" }, + { name: "Placeholder 3", href: "#" }, + { name: "Contact", href: "/#contact-section", isContactLink: true }, +]; diff --git a/lib/constants/urls.ts b/lib/constants/urls.ts new file mode 100644 index 0000000..4ee8882 --- /dev/null +++ b/lib/constants/urls.ts @@ -0,0 +1,3 @@ +export const EXTERNAL_URLS = { + spotifyProfile: "https://open.spotify.com/user/223qjmi62hl4nilhilo6lsdea", +} as const; diff --git a/lib/schemas/contact.ts b/lib/schemas/contact.ts new file mode 100644 index 0000000..3d6e309 --- /dev/null +++ b/lib/schemas/contact.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const contactFormSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters long"), + email: z.string().email("Invalid email address"), + message: z.string().min(10, "Message must be at least 10 characters long"), +}); + +export type ContactFormData = z.infer; diff --git a/lib/socialMedia.ts b/lib/socialMedia.ts index 0460e24..b9c3404 100644 --- a/lib/socialMedia.ts +++ b/lib/socialMedia.ts @@ -1,41 +1,35 @@ -interface socialMedia { - name: string; - url: string; - icon: string; - isBlack?: boolean; -} +import type { SocialMediaItem } from "@/lib/types"; +import { ASSETS } from "@/lib/constants"; -const baseUrl = "/svgs/"; - -export const socialMedia: socialMedia[] = [ - { - name: "GitHub", - url: `${process.env.GitHub_URL}`, - icon: `${baseUrl}github.svg`, - isBlack: true, - }, - { - name: "LinkedIn", - url: `${process.env.LinkedIn_URL}`, - icon: `${baseUrl}linkedin.svg`, - isBlack: true, - }, - { - name: "Personal Website", - url: `${process.env.Personal_Website_URL}`, - icon: `${baseUrl}earth.svg`, - isBlack: true, - }, - { - name: "Discord", - url: `${process.env.Discord_Profile}`, - icon: `${baseUrl}discord.svg`, - isBlack: true, - }, - { - name: "Youtube", - url: `${process.env.Youtube_URL}`, - icon: `${baseUrl}youtube.svg`, - isBlack: true, - } -]; \ No newline at end of file +export const socialMedia: SocialMediaItem[] = [ + { + name: "GitHub", + url: `${process.env.GitHub_URL}`, + icon: `${ASSETS.svgBasePath}github.svg`, + isBlack: true, + }, + { + name: "LinkedIn", + url: `${process.env.LinkedIn_URL}`, + icon: `${ASSETS.svgBasePath}linkedin.svg`, + isBlack: true, + }, + { + name: "Personal Website", + url: `${process.env.Personal_Website_URL}`, + icon: `${ASSETS.svgBasePath}earth.svg`, + isBlack: true, + }, + { + name: "Discord", + url: `${process.env.Discord_Profile}`, + icon: `${ASSETS.svgBasePath}discord.svg`, + isBlack: true, + }, + { + name: "Youtube", + url: `${process.env.Youtube_URL}`, + icon: `${ASSETS.svgBasePath}youtube.svg`, + isBlack: true, + }, +]; diff --git a/lib/types/index.ts b/lib/types/index.ts new file mode 100644 index 0000000..afc5c8d --- /dev/null +++ b/lib/types/index.ts @@ -0,0 +1,22 @@ +export interface SocialMediaItem { + name: string; + url: string; + icon: string; + isBlack?: boolean; +} + +export interface SpotifyStatus { + isListening: boolean; + trackName?: string; + artistName?: string; + message?: string; + itemType?: "track" | "episode"; +} + +export interface NavLinkItem { + name: string; + href: string; + target?: string; + rel?: string; + isContactLink?: boolean; +} diff --git a/proxy.ts b/proxy.ts index a68ca4e..bbaa6fe 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { headers } from "next/headers"; -import { auth } from "@/lib/auth"; +import { auth } from "@/lib/auth/auth"; export async function proxy(request: NextRequest) { const session = await auth.api.getSession({ diff --git a/public/images/CoC Logo.jpeg b/public/images/CoC Logo.jpeg new file mode 100644 index 0000000..8b9a3a2 Binary files /dev/null and b/public/images/CoC Logo.jpeg differ diff --git a/public/svgs/X.svg b/public/svgs/X.svg new file mode 100644 index 0000000..437e2bf --- /dev/null +++ b/public/svgs/X.svg @@ -0,0 +1,3 @@ + + +