Initial commit: Full Circle Digital Marketing website new

This commit is contained in:
Jeff Emmett 2025-06-29 22:27:24 -04:00
commit 0a022e9dfa
216 changed files with 7742 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# next.js
/.next/
/out/
# production
/build
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

120
app/about/page.tsx Normal file
View File

@ -0,0 +1,120 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import Image from "next/image"
import Link from "next/link"
export default function AboutPage() {
return (
<div className="min-h-screen">
<Header />
{/* Hero Header Section with Palm Trees Background */}
<section className="relative py-32 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/full-circle-digital-marketing-img-2.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
{/* About Us Title in Permanent Marker */}
<h1 className="section-title text-6xl mb-8 text-white">About Us</h1>
{/* Intro Text in Oswald */}
<p className="text-xl md:text-2xl mb-8 max-w-4xl mx-auto leading-relaxed">
We're here to amplify messages we believe in. If your product or service provides an unmistakable benefit to
your customers and the world around them, we want to see you succeed. Get to know us below, and get in
touch!
</p>
</div>
</section>
{/* Main Content */}
<main className="py-20 bg-white">
<div className="container mx-auto px-4">
{/* MEET THE TEAM SECTION */}
<div className="max-w-6xl mx-auto">
<h3 className="section-title text-4xl text-center mb-16 text-gray-900">Meet the Team</h3>
{/* TWO-COLUMN TEAM LAYOUT */}
<div className="grid md:grid-cols-2 gap-16 max-w-5xl mx-auto">
{/* JEFF EMMETT - Left column */}
<div className="text-center bg-white">
{/* SQUARE PHOTO */}
<div className="mb-8">
<Image
src="/images/jeff-emmett.jpg"
alt="Jeff Emmett"
width={320}
height={320}
className="mx-auto rounded-lg object-cover w-80 h-80 shadow-lg border-4 border-gray-100"
/>
</div>
{/* NAME in Permanent Marker */}
<h3 className="service-title text-3xl mb-3 text-gray-900">Jeff Emmett</h3>
{/* FOUNDER TITLE in italics */}
<p className="text-gray-600 italic mb-6 text-lg">Founder</p>
{/* BIO TEXT */}
<p className="text-gray-700 leading-relaxed text-base px-4">
With over 10 years of Social Media & Digital Marketing Strategy experience, Jeff is a self-professed
"idea guy", and knows how to implement them. Having planned & built out widely varying campaigns
across multiple platforms and industry verticals, he's learned the importance of trial and error in
making digital strategies succeed.
</p>
</div>
{/* MARCO BENINATO - Right column */}
<div className="text-center bg-white">
{/* SQUARE PHOTO */}
<div className="mb-8">
<Image
src="/images/marco-beninato.jpg"
alt="Marco Beninato"
width={320}
height={320}
className="mx-auto rounded-lg object-cover w-80 h-80 shadow-lg border-4 border-gray-100"
/>
</div>
{/* NAME in Permanent Marker */}
<h3 className="service-title text-3xl mb-3 text-gray-900">Marco Beninato</h3>
{/* LOGISTICS COORDINATOR TITLE in italics */}
<p className="text-gray-600 italic mb-6 text-lg">Logistics Coordinator</p>
{/* BIO TEXT */}
<p className="text-gray-700 leading-relaxed text-base px-4">Lorem Ipsum</p>
</div>
</div>
</div>
</div>
</main>
{/* CTA Section - Ready to get noticed online? */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h2 className="section-title text-4xl mb-6">Ready to get noticed online?</h2>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Great! If you'd like to learn more about how we can help you get noticed online, click the button below to
schedule an initial consultation phone call.
</p>
<Button
asChild
size="lg"
variant="outline"
className="bg-transparent border-white text-white hover:bg-white hover:text-teal-600"
>
<Link href="/contact">Get in Touch</Link>
</Button>
</div>
</section>
<Footer />
</div>
)
}

View File

61
app/contact/actions.ts Normal file
View File

@ -0,0 +1,61 @@
"use server"
import { redirect } from "next/navigation"
export async function sendContactEmail(formData: FormData) {
const name = formData.get("name") as string
const email = formData.get("email") as string
const phone = formData.get("phone") as string
const company = formData.get("company") as string
const message = formData.get("message") as string
// Email configuration
const recipientEmail = "info@fullcircledigitalmarketing.ca"
// Create email content
const emailSubject = `New Contact Form Submission from ${name}`
const emailBody = `
New contact form submission from your website:
Name: ${name}
Email: ${email}
Phone: ${phone || "Not provided"}
Company: ${company || "Not provided"}
Message:
${message}
---
This message was sent from the Full Circle Digital Marketing contact form.
`
try {
// In a real implementation, you would use a service like Resend, SendGrid, or similar
// For now, we'll simulate the email sending
console.log("Sending email to:", recipientEmail)
console.log("Subject:", emailSubject)
console.log("Body:", emailBody)
// Simulate email sending delay
await new Promise((resolve) => setTimeout(resolve, 1000))
// In production, you would implement actual email sending here:
/*
await resend.emails.send({
from: 'noreply@fullcircledigitalmarketing.ca',
to: recipientEmail,
subject: emailSubject,
text: emailBody,
replyTo: email,
})
*/
console.log("Email sent successfully!")
} catch (error) {
console.error("Failed to send email:", error)
throw new Error("Failed to send message. Please try again.")
}
// Redirect to a thank you page or back to contact with success message
redirect("/contact?success=true")
}

View File

143
app/contact/page.tsx Normal file
View File

@ -0,0 +1,143 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { sendContactEmail } from "./actions"
import { Suspense } from "react"
function SuccessMessage({ searchParams }: { searchParams: { success?: string } }) {
if (searchParams.success === "true") {
return (
<div className="max-w-2xl mx-auto mb-8">
<div className="bg-green-50 border border-green-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800">Message sent successfully!</h3>
<div className="mt-2 text-sm text-green-700">
<p>Thank you for contacting us. We'll get back to you within 24 hours.</p>
</div>
</div>
</div>
</div>
</div>
)
}
return null
}
export default function ContactPage({ searchParams }: { searchParams: { success?: string } }) {
return (
<div className="min-h-screen">
<Header />
{/* Hero Section */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/contact-hero.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h1 className="text-5xl mb-4 text-white font-bold" style={{ fontFamily: "var(--font-permanent-marker)" }}>
Contact Us
</h1>
</div>
</section>
<main className="py-20">
<div className="container mx-auto px-4">
<Suspense fallback={null}>
<SuccessMessage searchParams={searchParams} />
</Suspense>
<div className="max-w-2xl mx-auto">
<Card className="p-8">
<CardContent>
<h2 className="text-2xl font-semibold mb-6 text-center">Ready to get noticed online?</h2>
<p className="text-gray-700 mb-8 text-center">
Great! If you'd like to learn more about how we can help you get noticed online, we'd love to schedule
an initial consultation phone call.
</p>
<form action={sendContactEmail} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
Name *
</label>
<input
type="text"
id="name"
name="name"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<input
type="email"
id="email"
name="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-2">
Phone Number
</label>
<input
type="tel"
id="phone"
name="phone"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-2">
Company/Organization
</label>
<input
type="text"
id="company"
name="company"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2">
Tell us about your project *
</label>
<textarea
id="message"
name="message"
rows={4}
required
placeholder="What are your digital marketing goals? What challenges are you facing?"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
<Button type="submit" className="w-full bg-teal-600 hover:bg-teal-700">
Send Message
</Button>
</form>
</CardContent>
</Card>
</div>
</div>
</main>
<Footer />
</div>
)
}

View File

211
app/globals.css Normal file
View File

@ -0,0 +1,211 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--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%;
--font-oswald: var(--font-oswald);
--font-permanent-marker: var(--font-permanent-marker);
--font-playfair-display: var(--font-playfair-display);
--font-roboto: var(--font-roboto);
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-oswald), sans-serif;
font-weight: 300;
/* Ensure smooth font rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* All headings use Permanent Marker */
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-permanent-marker), cursive;
font-weight: 400;
/* Optimize font rendering for headings */
font-display: swap;
}
/* Navigation uses Oswald */
nav,
nav a {
font-family: var(--font-oswald), sans-serif;
font-weight: 300;
}
/* Ensure all text elements use Oswald by default */
p,
span,
div,
a,
button,
input,
textarea,
label {
font-family: var(--font-oswald), sans-serif;
font-weight: 300;
}
/* Specific font classes */
.font-oswald {
font-family: var(--font-oswald), sans-serif !important;
font-weight: 300 !important;
}
.font-roboto {
font-family: var(--font-roboto), sans-serif !important;
}
.font-permanent-marker {
font-family: var(--font-permanent-marker), cursive !important;
}
.font-playfair {
font-family: var(--font-playfair-display), serif !important;
}
/* Hero title - using Permanent Marker */
.hero-title {
font-family: var(--font-permanent-marker), cursive !important;
font-weight: 400 !important;
letter-spacing: -0.02em;
font-display: swap;
}
/* Section titles - using Permanent Marker */
.section-title {
font-family: var(--font-permanent-marker), cursive !important;
font-weight: 400 !important;
font-display: swap;
}
/* Service card titles - using Permanent Marker */
.service-title {
font-family: var(--font-permanent-marker), cursive !important;
font-weight: 400 !important;
font-display: swap;
}
/* Testimonial names - using Permanent Marker */
.testimonial-name {
font-family: var(--font-permanent-marker), cursive !important;
font-weight: 400 !important;
font-display: swap;
}
/* Prevent font flash during load */
.font-loading {
visibility: hidden;
}
.fonts-loaded .font-loading {
visibility: visible;
}
/* Custom height for larger logos */
.max-h-30 {
max-height: 7.5rem; /* 120px */
}
/* Text shadow */
.text-shadow-sm {
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
}
}
/* Critical font loading optimization */
@font-face {
font-family: "Oswald";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("https://fonts.gstatic.com/s/oswald/v53/TK3_WkUHHAIjg75cFRf3bXL8LICs1_FvsUtiZTaR.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Roboto";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Roboto";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("https://fonts.gstatic.com/s/roboto/v32/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

87
app/layout.tsx Normal file
View File

@ -0,0 +1,87 @@
import type React from "react"
import type { Metadata } from "next"
import { Oswald, Permanent_Marker, Playfair_Display, Roboto } from "next/font/google"
import "./globals.css"
const oswald = Oswald({
subsets: ["latin"],
weight: ["300"],
variable: "--font-oswald",
display: "swap",
})
const permanentMarker = Permanent_Marker({
subsets: ["latin"],
weight: ["400"],
variable: "--font-permanent-marker",
display: "swap",
})
const playfairDisplay = Playfair_Display({
subsets: ["latin"],
weight: ["400"],
variable: "--font-playfair-display",
display: "swap",
})
const roboto = Roboto({
subsets: ["latin"],
weight: ["300", "400", "500"],
variable: "--font-roboto",
display: "swap",
})
export const metadata: Metadata = {
title: "Full Circle Digital Marketing",
description: "We are a boutique digital marketing team committed to helping you succeed at what you do best.",
icons: {
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Full-Circle-Digital-Marketing-Submark-Web-OF9AEH5BhFbC3uaQ9le1QiIkUjPzdD.png",
apple:
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Full-Circle-Digital-Marketing-Submark-Web-OF9AEH5BhFbC3uaQ9le1QiIkUjPzdD.png",
},
generator: 'v0.dev'
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
{/* Preload critical fonts for faster rendering */}
<link
rel="preload"
href="https://fonts.gstatic.com/s/oswald/v53/TK3_WkUHHAIjg75cFRf3bXL8LICs1_FvsUtiZTaR.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://fonts.gstatic.com/s/roboto/v32/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* DNS prefetch for Google Fonts */}
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
{/* Preconnect for faster font loading */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body className={`${roboto.variable} ${oswald.variable} ${permanentMarker.variable} ${playfairDisplay.variable}`}>
{children}
</body>
</html>
)
}

View File

107
app/our-services/page.tsx Normal file
View File

@ -0,0 +1,107 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import Image from "next/image"
import Link from "next/link"
export default function ServicesPage() {
const services = [
{
title: "Integrated Digital Marketing Strategy",
description:
"A robust marketing strategy should be rolled out consistently across each avenue you use to converse with your customers. Our holistic approach to your digital success consists of the following components & more:",
features: [
"Marketing Funnel Discovery",
"Newsletter Automation Setup",
"Social Content Strategy",
"Promotion & Contest Planning",
],
icon: "/images/icons/marketing-strategy-green.png",
successStoryLink: "/success-stories/integrated-digital-marketing-strategy",
},
{
title: "Digital Media Buys",
description:
"Integrated, consistent cross platform messaging is essential to promoting your brand or product. Our media buy tactics work together with your organic post strategy and can incorporate the following channels:",
features: ["Facebook Advertising", "Instagram Advertising", "Youtube Advertising", "Search Advertising"],
icon: "/images/icons/media-buys-green.png",
successStoryLink: "/success-stories/digital-media-buys",
},
{
title: "Integrations, Analytics & Tracking",
description:
"An essential part of bringing your digital strategy full circle lies in tracking campaign progress, ensuring targets are being met, and iterating on the ideas that work. Cost per result varies across different industries, which is why we utilize a data based iteration model that suits the needs of any business, utilizing the platforms below and more:",
features: ["Google Analytics", "Tag Manager & Tracking Pixels", "Ad Campaign Reporting"],
icon: "/images/icons/integrations.jpg",
successStoryLink: "/success-stories/integrations-analytics-tracking",
},
]
return (
<div className="min-h-screen">
<Header />
{/* Page Header with New Mountain Background */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h1 className="text-5xl mb-4 text-white font-bold" style={{ fontFamily: "var(--font-permanent-marker)" }}>
Our Services
</h1>
</div>
</section>
<main className="py-20">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto space-y-16">
{services.map((service, index) => (
<div
key={index}
className={`grid md:grid-cols-2 gap-8 items-center ${index % 2 === 1 ? "md:grid-flow-col-dense" : ""}`}
>
<div className={`${index % 2 === 1 ? "md:col-start-2" : ""}`}>
<Image
src={service.icon || "/placeholder.svg"}
alt={service.title}
width={400}
height={245}
className="mx-auto"
/>
</div>
<div className={`${index % 2 === 1 ? "md:col-start-1" : ""}`}>
<h2
className="text-3xl mb-4 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
{service.title}
</h2>
<p className="text-lg text-gray-700 mb-6 leading-relaxed">{service.description}</p>
<ul className="list-none text-gray-700 mb-6 space-y-2">
{service.features.map((feature, featureIndex) => (
<li key={featureIndex} className="flex items-start">
<span className="text-teal-600 mr-2"></span>
<span>{feature}</span>
</li>
))}
</ul>
<div className="text-center">
<Button asChild className="bg-teal-600 hover:bg-teal-700 text-white">
<Link href={service.successStoryLink}>See our Success Story</Link>
</Button>
</div>
</div>
</div>
))}
</div>
</div>
</main>
<Footer />
</div>
)
}

300
app/page.tsx Normal file
View File

@ -0,0 +1,300 @@
import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { TestimonialSlider } from "@/components/testimonial-slider"
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { FontLoader } from "@/components/font-loader"
export default function HomePage() {
return (
<div className="min-h-screen">
<FontLoader />
<Header />
{/* Hero Section with New Mountain Background */}
<section className="relative h-screen flex items-center justify-center text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 text-center max-w-4xl mx-auto px-4">
<h1 className="hero-title text-5xl md:text-6xl mb-6 leading-tight font-loading">
Bring your digital marketing strategy full circle
</h1>
<p className="text-xl md:text-2xl mb-8 max-w-3xl mx-auto font-loading">
We are a boutique digital marketing team committed to helping you succeed at what <em>you</em> do best.
</p>
<Button
asChild
size="lg"
variant="outline"
className="bg-transparent border-white text-white hover:bg-white hover:text-teal-600 font-loading"
>
<Link href="/our-services">See our Services</Link>
</Button>
</div>
</section>
{/* Services Section - FIXED TEXT VISIBILITY */}
<section className="py-20 bg-gray-50">
<div className="container mx-auto px-4">
<div className="text-center mb-16">
<h2
className="text-4xl font-bold text-gray-900 mb-4"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Digital Marketing Solutions for Your Business
</h2>
</div>
<div className="grid md:grid-cols-1 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
{/* Integrated Digital Marketing Strategy */}
<Card className="text-center p-6 border-none shadow-lg h-full">
<CardContent className="pt-6">
<div className="mb-6 h-32 flex items-center justify-center">
<Image
src="/images/icons/marketing-strategy-green.png"
alt="Integrated Digital Marketing Strategy"
width={120}
height={120}
className="mx-auto"
/>
</div>
<h3
className="text-xl mb-4 font-bold text-gray-900"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Integrated Digital Marketing Strategy
</h3>
<p className="text-gray-600 mb-4 text-base leading-relaxed">
A robust marketing strategy should be rolled out consistently across each avenue you use to converse
with your customers. Our holistic approach to your digital success consists of the following
components & more:
</p>
<ul className="text-left text-gray-600 mb-6 space-y-2 text-sm">
<li> Marketing Funnel Discovery</li>
<li> Newsletter Automation Setup</li>
<li> Social Content Strategy</li>
<li> Promotion & Contest Planning</li>
</ul>
<Button asChild className="bg-teal-600 hover:bg-teal-700 text-white">
<Link href="/success-stories/integrated-digital-marketing-strategy">See our Success Story</Link>
</Button>
</CardContent>
</Card>
{/* Digital Media Buys */}
<Card className="text-center p-6 border-none shadow-lg h-full">
<CardContent className="pt-6">
<div className="mb-6 h-32 flex items-center justify-center">
<Image
src="/images/icons/media-buys-green.png"
alt="Digital Media Buys"
width={120}
height={120}
className="mx-auto"
/>
</div>
<h3
className="text-xl mb-4 font-bold text-gray-900"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Digital Media Buys
</h3>
<p className="text-gray-600 mb-4 text-base leading-relaxed">
Integrated, consistent cross platform messaging is essential to promoting your brand or product. Our
media buy tactics work together with your organic post strategy and can incorporate the following
channels:
</p>
<ul className="text-left text-gray-600 mb-6 space-y-2 text-sm">
<li> Facebook Advertising</li>
<li> Instagram Advertising</li>
<li> Youtube Advertising</li>
<li> Search Advertising</li>
</ul>
<Button asChild className="bg-teal-600 hover:bg-teal-700 text-white">
<Link href="/success-stories/digital-media-buys">See our Success Story</Link>
</Button>
</CardContent>
</Card>
{/* Integrations, Analytics & Tracking */}
<Card className="text-center p-6 border-none shadow-lg h-full">
<CardContent className="pt-6">
<div className="mb-6 h-32 flex items-center justify-center">
<Image
src="/images/icons/integrations.jpg"
alt="Integrations, Analytics & Tracking"
width={120}
height={120}
className="mx-auto"
/>
</div>
<h3
className="text-xl mb-4 font-bold text-gray-900"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Integrations, Analytics & Tracking
</h3>
<p className="text-gray-600 mb-4 text-base leading-relaxed">
An essential part of bringing your digital strategy full circle lies in tracking campaign progress,
ensuring targets are being met, and iterating on the ideas that work. Cost per result varies across
different industries, which is why we utilize a data based iteration model that suits the needs of any
business, utilizing the platforms below and more:
</p>
<ul className="text-left text-gray-600 mb-6 space-y-2 text-sm">
<li> Google Analytics</li>
<li> Tag Manager & Tracking Pixels</li>
<li> Ad Campaign Reporting</li>
</ul>
<Button asChild className="bg-teal-600 hover:bg-teal-700 text-white">
<Link href="/success-stories/integrations-analytics-tracking">See our Success Story</Link>
</Button>
</CardContent>
</Card>
</div>
</div>
</section>
{/* Testimonials Section */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4">
<h2 className="section-title text-4xl text-center mb-12 font-loading">Testimonials about Our Work</h2>
<TestimonialSlider />
</div>
</section>
{/* Client Logos Section */}
<section className="py-16 bg-white">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<Image
src="/images/clients-header-hd.png"
alt="Some of Our Clients"
width={600}
height={60}
className="mx-auto mb-8 h-auto max-w-2xl"
/>
</div>
<div className="flex flex-wrap justify-center items-center gap-12 max-w-6xl mx-auto">
<Link
href="https://freeandeasytraveler.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/free-and-easy-traveler.jpg"
alt="Free & Easy Traveler"
width={210}
height={210}
className="max-h-30 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
<Link
href="https://breatheinlife.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/breathe-in-life.jpg"
alt="Breathe in Life"
width={210}
height={210}
className="max-h-30 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
<Link
href="https://seekers-media.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/seekers-media.jpg"
alt="Seekers Media"
width={210}
height={210}
className="max-h-30 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
<Link
href="https://pilateswithfadia.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/pilates-with-fadia-logo.png"
alt="Pilates with Fadia"
width={210}
height={210}
className="max-h-30 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
</div>
</div>
</section>
{/* About Section */}
<section className="py-20 bg-white">
<div className="container mx-auto px-4 text-center">
<h2 className="section-title text-4xl text-gray-900 mb-8 font-loading">Who We Are</h2>
<div className="max-w-4xl mx-auto">
<p className="text-lg text-gray-700 mb-8 leading-relaxed font-loading">
Full Circle Digital Marketing came together to amplify messages that we believe in. Although clicks and
channel growth are a necessary component of any digital marketing strategy, we believe there is a more
essential, real world impact when a good idea whose time has come hits the mainstream. We aren't your
typical marketing types - we're not in this for the sake of mindless engagement, but for the
people-oriented results we see when a good message spreads.
</p>
<Button asChild size="lg" className="bg-teal-600 hover:bg-teal-700 font-loading">
<Link href="/about">Learn More About Us</Link>
</Button>
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h2 className="section-title text-4xl mb-6 font-loading">Ready to get noticed online?</h2>
<p className="text-xl mb-8 max-w-2xl mx-auto font-loading">
Great! If you'd like to learn more about how we can help you get noticed online, click the button below to
schedule an initial consultation phone call.
</p>
<Button
asChild
size="lg"
variant="outline"
className="bg-transparent border-white text-white hover:bg-white hover:text-teal-600 font-loading"
>
<Link href="/contact">Get in Touch</Link>
</Button>
</div>
</section>
<Footer />
</div>
)
}

View File

View File

@ -0,0 +1,187 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import Image from "next/image"
import Link from "next/link"
export default function MediaBuysPage() {
return (
<div className="min-h-screen">
<Header />
{/* Page Header */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h1
className="text-4xl md:text-5xl mb-4 text-white font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Success Story #2: Digital Media Buys
</h1>
</div>
</section>
<main className="py-20">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto">
<div className="space-y-16">
{/* Client Problem */}
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<Image
src="/images/icons/client-problem-icon.png"
alt="Client Problem"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div>
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Client Problem:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Our client was interested in increasing reach and click throughs for their content on Facebook. They
had clear targets for desired cost per click, but were having trouble achieving consistent results
through Facebook advertising.
</p>
</div>
</div>
{/* Our Plan */}
<div className="grid md:grid-cols-2 gap-8 items-center md:grid-flow-col-dense">
<div className="md:col-start-2">
<Image
src="/images/icons/our-plan-icon.png"
alt="Our Plan"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div className="md:col-start-1">
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Our Plan:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Given the hard & fast targets our client would be measuring ad campaign success by, we planned to
experiment with different media such as static images, slideshows, and short & long form video to
determine which media formats provided better results. We would take an iterative, data-based
approach to replicate content that performed better throughout our experimentation phase. We would
re-vamp the audiences targeted by the advertised content to ensure a close demographic fit. Our
campaign structure would be modeled to closely follow the naming convention of the client's
expeditions for consistency, and links to all active ads would be included in the client's campaign
spreadsheet to allow them a first-person perspective on ad performance.
</p>
</div>
</div>
{/* Implementation */}
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<Image
src="/images/icons/implementation-icon.png"
alt="Implementation"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div>
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Implementation:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Our first steps involved looking at the client's past campaigns, to determine what strategies had
worked and which had underperformed. We decided to move away from short-term giveaway based posts &
ads, as they garnered very little engagement and incurred large costs. We suggested the client move
towards a 'slow and steady' ad approach, that had reasonable daily budgets to avoid high display
frequency, and cut down on cost per click at the same time. This had the added benefit of widening
our campaign planning window, which allowed us to be more prepared for ad campaigns that were coming
down the pipe. There were many lessons learned through our first experimental rounds of advertising,
and within two or three iterations of tweaking ad sets and applying what worked from across the
account, we were seeing consistent cost per click (CPC) that was well within the client's targeted
range.
</p>
</div>
</div>
{/* Results */}
<div className="grid md:grid-cols-2 gap-8 items-center md:grid-flow-col-dense">
<div className="md:col-start-2">
<Image
src="/images/icons/results-icon.png"
alt="Results"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div className="md:col-start-1">
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Results:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
We have successfully run dozens of ad campaigns for our client, consistently providing 125%+ of
their expected click throughs on each campaign. Not only are we exceeding the expected readership on
the client's content, but we are providing these results at up to 50% under the client's expected
budget.
</p>
</div>
</div>
</div>
{/* CTA Section */}
</div>
</div>
</main>
{/* CTA Section - Full Width */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/cta-hero.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h2 className="section-title text-4xl mb-6">Ready to get noticed online?</h2>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Great! If you'd like to learn more about how we can help you get noticed online, click the button below to
schedule an initial consultation phone call.
</p>
<Button
asChild
size="lg"
variant="outline"
className="bg-transparent border-white text-white hover:bg-white hover:text-teal-600"
>
<Link href="/contact">Get in Touch</Link>
</Button>
</div>
</section>
<Footer />
</div>
)
}

View File

@ -0,0 +1,189 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import Image from "next/image"
import Link from "next/link"
export default function IntegratedStrategyPage() {
return (
<div className="min-h-screen">
<Header />
{/* Page Header */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h1
className="text-4xl md:text-5xl mb-4 text-white font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Success Story #1: Integrated Digital Marketing Strategy
</h1>
</div>
</section>
<main className="py-20">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto">
<div className="space-y-16">
{/* Client Problem */}
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<Image
src="/images/icons/client-problem-icon.png"
alt="Client Problem"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div>
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Client Problem:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Our client had very little experience with digital strategy and cohesion of brand message and
campaigns, and no known key performance indicators (KPIs) to track and measure results, or determine
their direction moving forward. Our client was in need of a holistic digital marketing strategy.
</p>
</div>
</div>
{/* Our Plan */}
<div className="grid md:grid-cols-2 gap-8 items-center md:grid-flow-col-dense">
<div className="md:col-start-2">
<Image
src="/images/icons/our-plan-icon.png"
alt="Our Plan"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div className="md:col-start-1">
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Our Plan:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Construct a custom made digital funnel strategy, integrating various platforms to synthesize a
consistent brand message across all channels, ads and campaigns. The system will continuously
generate new leads from a cold audience, warm them up through automated email and advertising
processes, and offer lower funnel incentives through evergreen remarketing campaigns to encourage
conversion. The system will essentially offer funnel-appropriate messaging for the entire customer
life cycle on all appropriate customer channels. Our aim is to provide timely, relevant and helpful
information to customers as they progress through the cycle of discovering, learning about, trusting
and converting with the client's brand.
</p>
</div>
</div>
{/* Implementation */}
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<Image
src="/images/icons/implementation-icon.png"
alt="Implementation"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div>
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Implementation:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Our integration of client platforms included Facebook, Instagram, MailChimp, Google Ads, Analytics,
MsgHero, Basecamp, Olark, and a custom database, most of which occurred through Zapier. We rolled
out campaigns by funnel level, with Reach and Page Like ads via Facebook and Instagram to reach high
funnel level cold leads and encourage engagement with inspiring content. We focused webclick ads to
retarget the middle funnel (i.e. the people engaging with our high level content, such as website
visitors), delivering targeted messaging towards people who may be deciding between various brands.
For our lower level audience who were closest to conversion (i.e. audiences who have spent the most
time on our site), we put out conversion oriented content such as promotions, messenger ads, FAQs to
answer last minute questions, and so on. We created stats reporting dashboards and a procedure for
monthly updates on budget and KPIs for each funnel level.
</p>
</div>
</div>
{/* Results */}
<div className="grid md:grid-cols-2 gap-8 items-center md:grid-flow-col-dense">
<div className="md:col-start-2">
<Image
src="/images/icons/results-icon.png"
alt="Results"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div className="md:col-start-1">
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Results:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
The client was very happy with the continuous growth seen on all channels. Newsletter growth was
+200 subscribers/week, Facebook and Instagram channels were growing on the order of 1500 - 6000
followers per month, and conversions were coming in within our target ROI of $150 per customer. Our
return on ad spend (ROAS) was consistently sitting around 2000%. Some of our lower level specific
content was bringing in conversions for as little as $5 to small, targeted audiences. Mission
accomplished!
</p>
</div>
</div>
</div>
{/* CTA Section */}
</div>
</div>
</main>
{/* CTA Section - Full Width */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/cta-hero.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h2 className="section-title text-4xl mb-6">Ready to get noticed online?</h2>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Great! If you'd like to learn more about how we can help you get noticed online, click the button below to
schedule an initial consultation phone call.
</p>
<Button
asChild
size="lg"
variant="outline"
className="bg-transparent border-white text-white hover:bg-white hover:text-teal-600"
>
<Link href="/contact">Get in Touch</Link>
</Button>
</div>
</section>
<Footer />
</div>
)
}

View File

@ -0,0 +1,183 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import Image from "next/image"
import Link from "next/link"
export default function IntegrationsPage() {
return (
<div className="min-h-screen">
<Header />
{/* Page Header */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h1
className="text-4xl md:text-5xl mb-4 text-white font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Success Story #3: Integrations, Analytics & Tracking
</h1>
</div>
</section>
<main className="py-20">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto">
<div className="space-y-16">
{/* Client Problem */}
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<Image
src="/images/icons/client-problem-icon.png"
alt="Client Problem"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div>
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Client Problem:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Our client was overwhelmed with data flowing from various ad managers, analytics platforms, and
their own reports. They were having a hard time determining which metrics needed their attention,
and what could be done to improve on those results in future campaigns.
</p>
</div>
</div>
{/* Our Plan */}
<div className="grid md:grid-cols-2 gap-8 items-center md:grid-flow-col-dense">
<div className="md:col-start-2">
<Image
src="/images/icons/our-plan-icon.png"
alt="Our Plan"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div className="md:col-start-1">
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Our Plan:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
As part of our discovery process, we planned to meet with the client to better understand the
customer journey experienced by users of our client's service. We then planned to map each stage of
customer acquisition onto a marketing funnel consisting of awareness (high level), consideration
(mid level) and conversion (low level), breaking each step down into multiple micro-goals that could
be used to identify problems in achieving the client's major targets.
</p>
</div>
</div>
{/* Implementation */}
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<Image
src="/images/icons/implementation-icon.png"
alt="Implementation"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div>
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Implementation:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
Organizing large amounts of information requires some creativity to present the data in a clear &
relevant way. In our reporting dashboard, we colour-coded each data stream to coordinate with its
level in the marketing funnel. With this system, the client's management team could easily pick out
key performance indicators, while department managers were able to focus on micro-goals more
relevant to their departments. We also included the client's budgeting and optimal targets on the
reporting dashboard, so that they could understand their financial investment and success at
reaching their targets at a glance. We provided the client with bi-weekly updates on all performance
indicators to allow for regular improvements to their digital marketing efforts.
</p>
</div>
</div>
{/* Results */}
<div className="grid md:grid-cols-2 gap-8 items-center md:grid-flow-col-dense">
<div className="md:col-start-2">
<Image
src="/images/icons/results-icon.png"
alt="Results"
width={300}
height={300}
className="mx-auto"
/>
</div>
<div className="md:col-start-1">
<h2
className="text-3xl mb-6 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
Results:
</h2>
<p className="text-lg text-gray-700 leading-relaxed">
The client was extremely satisfied with the analytics & tracking dashboard that we provided them.
Their ability to understand the effect of their online campaigns immediately after they concluded
allowed them to determine which tactics and campaigns were more effective in helping them reach
their goals. We were able to feed back the knowledge gained from our tracking dashboard into newly
launched campaigns to improve the client's return on investment.
</p>
</div>
</div>
</div>
{/* CTA Section */}
</div>
</div>
</main>
{/* CTA Section - Full Width */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/cta-hero.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h2 className="section-title text-4xl mb-6">Ready to get noticed online?</h2>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Great! If you'd like to learn more about how we can help you get noticed online, click the button below to
schedule an initial consultation phone call.
</p>
<Button
asChild
size="lg"
variant="outline"
className="bg-transparent border-white text-white hover:bg-white hover:text-teal-600"
>
<Link href="/contact">Get in Touch</Link>
</Button>
</div>
</section>
<Footer />
</div>
)
}

View File

@ -0,0 +1,162 @@
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import Image from "next/image"
import Link from "next/link"
export default function SuccessStoriesPage() {
const successStories = [
{
title: "Success Story #1: Integrated Digital Marketing Strategy",
description:
"See how our holistic digital strategy helped a business like yours to achieve remarkable growth. Increase your brand awareness, maintain a steady stream of new, interested customers, and communicate consistently to convert and deliver value to your customer base with a plan that takes your strategy Full Circle.",
icon: "/images/icons/marketing-strategy-green.png",
link: "/success-stories/integrated-digital-marketing-strategy",
},
{
title: "Success Story #2: Digital Media Buys",
description:
"Find out how our iterative, data-based approach to setting and reaching targets with social media ads produces results you can understand and be confident in. Read more to see how we helped build a sustainable and consistent online audience for a client's content.",
icon: "/images/icons/media-buys-green.png",
link: "/success-stories/digital-media-buys",
},
{
title: "Success Story #3: Integrations, Analytics & Tracking",
description:
"The world of online marketing can be confusing. Read more about our team's experience sifting through all the myriad data from your platforms and breaking it down into actionable insights to grow & improve your strategy every step of the way.",
icon: "/images/icons/integrations.jpg",
link: "/success-stories/integrations-analytics-tracking",
},
]
return (
<div className="min-h-screen">
<Header />
{/* Page Header with New Mountain Background */}
<section className="relative py-20 text-white">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background-new.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/90 to-blue-900/90" />
<div className="relative z-10 container mx-auto px-4 text-center">
<h1 className="text-5xl mb-4 text-white font-bold" style={{ fontFamily: "var(--font-permanent-marker)" }}>
Success Stories
</h1>
</div>
</section>
<main className="py-20">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto space-y-16">
{successStories.map((story, index) => (
<div
key={index}
className={`grid md:grid-cols-2 gap-8 items-center ${index % 2 === 1 ? "md:grid-flow-col-dense" : ""}`}
>
<div className={`${index % 2 === 1 ? "md:col-start-2" : ""}`}>
<Image
src={story.icon || "/placeholder.svg"}
alt={story.title}
width={400}
height={245}
className="mx-auto"
/>
</div>
<div className={`${index % 2 === 1 ? "md:col-start-1" : ""}`}>
<h3
className="text-2xl mb-4 text-gray-900 font-bold"
style={{ fontFamily: "var(--font-permanent-marker)" }}
>
{story.title}
</h3>
<p className="text-lg text-gray-700 mb-6 leading-relaxed">{story.description}</p>
<div className="text-center">
<Button asChild className="bg-teal-600 hover:bg-teal-700 text-white">
<Link href={story.link}>Read More</Link>
</Button>
</div>
</div>
</div>
))}
</div>
{/* Client Logos Section */}
<div className="mt-20 pt-16 border-t border-gray-300">
<div className="text-center mb-12">
<Image
src="/images/clients-header-hd.png"
alt="Some of Our Clients"
width={600}
height={60}
className="mx-auto mb-8 h-auto max-w-2xl"
/>
</div>
<div className="flex flex-wrap justify-center items-center gap-12 max-w-4xl mx-auto">
<Link
href="https://freeandeasytraveler.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/free-and-easy-traveler.jpg"
alt="Free & Easy Traveler"
width={180}
height={180}
className="max-h-24 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
<Link
href="https://breatheinlife.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/breathe-in-life.jpg"
alt="Breathe in Life"
width={180}
height={180}
className="max-h-24 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
<Link
href="https://seekers-media.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/seekers-media.jpg"
alt="Seekers Media"
width={180}
height={180}
className="max-h-24 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
<Link
href="https://pilateswithfadia.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-8 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow group"
>
<Image
src="/images/pilates-with-fadia-logo.png"
alt="Pilates with Fadia"
width={180}
height={180}
className="max-h-24 w-auto object-contain group-hover:scale-105 transition-transform"
/>
</Link>
</div>
</div>
</div>
</main>
<Footer />
</div>
)
}

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

View File

@ -0,0 +1,25 @@
"use client"
import { useEffect } from "react"
export function FontLoader() {
useEffect(() => {
// Check if fonts are loaded and add class to body
const checkFontsLoaded = () => {
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
document.body.classList.add("fonts-loaded")
})
} else {
// Fallback for browsers without Font Loading API
setTimeout(() => {
document.body.classList.add("fonts-loaded")
}, 100)
}
}
checkFontsLoaded()
}, [])
return null
}

83
components/footer.tsx Normal file
View File

@ -0,0 +1,83 @@
import Link from "next/link"
import { Linkedin } from "lucide-react"
export function Footer() {
return (
<footer className="bg-gray-900 text-white">
<div className="container mx-auto px-4 py-12">
<div className="grid md:grid-cols-4 gap-8">
{/* Social Links */}
<div>
<h3 className="text-lg font-semibold mb-4">Connect with Us</h3>
<Link
href="https://www.linkedin.com/company/full-circle-digital-marketing-services/"
target="_blank"
className="inline-flex items-center text-teal-400 hover:text-teal-300 transition-colors"
>
<Linkedin className="h-6 w-6" />
<span className="sr-only">LinkedIn</span>
</Link>
</div>
{/* Company Info */}
<div>
<h3 className="text-lg font-semibold mb-4">Full Circle Digital Marketing</h3>
<p className="text-gray-300">
We are a boutique digital marketing team committed to helping you succeed at what <em>you</em> do best.
</p>
</div>
{/* Navigation */}
<div>
<h3 className="text-lg font-semibold mb-4">Learn More</h3>
<div className="space-y-2">
<Link href="/about" className="block text-gray-300 hover:text-white transition-colors">
About Us
</Link>
<Link href="/our-services" className="block text-gray-300 hover:text-white transition-colors">
Our Services
</Link>
<Link href="/success-stories" className="block text-gray-300 hover:text-white transition-colors">
Success Stories
</Link>
<Link href="/contact" className="block text-gray-300 hover:text-white transition-colors">
Contact Us
</Link>
</div>
</div>
{/* Search */}
<div>
<h3 className="text-lg font-semibold mb-4">Looking for something in particular?</h3>
<form className="flex">
<input
type="search"
placeholder="Search this website..."
className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-l-md text-white placeholder-gray-400 focus:outline-none focus:border-teal-500"
/>
<button
type="submit"
className="px-4 py-2 bg-teal-600 text-white rounded-r-md hover:bg-teal-700 transition-colors"
>
Search
</button>
</form>
</div>
</div>
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<p>
Full Circle Digital Marketing © 2024 · Site Crafted By{" "}
<Link href="https://kaitschmidek.com/" target="_blank" className="text-teal-400 hover:text-teal-300">
Kait Schmidek
</Link>{" "}
· Theme by{" "}
<Link href="https://seothemes.com/" target="_blank" className="text-teal-400 hover:text-teal-300">
SEO Themes
</Link>
</p>
</div>
</div>
</footer>
)
}

View File

84
components/header.tsx Normal file
View File

@ -0,0 +1,84 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import Image from "next/image"
import { Menu } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
export function Header() {
const [isOpen, setIsOpen] = useState(false)
const navigation = [
{ name: "Home", href: "/" },
{ name: "About Us", href: "/about" },
{ name: "Our Services", href: "/our-services" },
{ name: "Success Stories", href: "/success-stories" },
{ name: "Contact Us", href: "/contact" },
]
return (
<header className="relative sticky top-0 z-50">
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url('/images/hero-background.jpg')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-teal-400/60 via-teal-500/50 to-teal-500/55" />
<div className="relative z-10 container mx-auto px-4">
<div className="flex items-center justify-between h-32">
{/* Logo */}
<Link href="/" className="flex items-center">
<Image
src="/images/full-circle-logo-black.png"
alt="Full Circle Digital Marketing"
width={400}
height={120}
className="h-24 w-auto"
/>
</Link>
{/* Desktop Navigation */}
<nav className="hidden lg:flex items-center space-x-8">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="text-white hover:text-teal-200 font-normal transition-colors tracking-wide text-xl"
style={{ fontFamily: "var(--font-oswald)" }}
>
{item.name}
</Link>
))}
</nav>
{/* Mobile Navigation */}
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild className="lg:hidden">
<Button variant="ghost" size="icon" className="text-white">
<Menu className="h-6 w-6" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px]">
<div className="flex flex-col space-y-4 mt-8">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="text-lg font-medium text-gray-700 hover:text-teal-600 transition-colors"
onClick={() => setIsOpen(false)}
>
{item.name}
</Link>
))}
</div>
</SheetContent>
</Sheet>
</div>
</div>
</header>
)
}

View File

View File

@ -0,0 +1,171 @@
"use client"
import { useState, useEffect } from "react"
import Image from "next/image"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
const testimonials = [
{
id: 1,
name: "Jacques Aucamp",
company: "Erika Wessels Jewellery",
image: "/images/jacques-aucamp.jpg",
quote:
"Full Circle has provided us with a comprehensive, fast and well planned digital marketing strategy that has yielded great results for us. Our ad campaigns and newsletters are performing exceptionally well in terms of cost per conversion which can only be attributed to Full Circle's attention to detail with the ad designs, copy-editing and carving up of the budgets for targeted ad groups. I highly recommend leveraging the Full Circle team to any business with a need for comprehensive digital marketing strategy!",
},
{
id: 2,
name: "Melissa Jol",
company: "Breathe In Life",
image:
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/melissa-croatia-e1533490318497-150x150-VCAO7rAsiHngO8bolJQdBxXnJzVTp1.jpeg",
quote:
"Full Circle is an amazing team to work with. Since we started with them, we have seen increases in sales, activity on social media, and overall inquiries about what we do and provide. That is due to their fantastic custom digital marketing strategy they created for our company and their personable approach to working with us. I highly recommend Full Circle not only for their brilliance in the marketing world but also because they are down to earth, kind, and very enjoyable to collaborate with.",
},
{
id: 3,
name: "Rob Campbell",
company: "Free & Easy Traveler",
image:
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Rob-Testimonial-pic-150x150.jpg-F9lYvo4gEHUI13t1Sl218LtCYgDX0H.jpeg",
quote:
"The team at Full Circle Digital Marketing are fantastic to work with. They were committed to fine tuning our strategy and with their skills and resources and were able to deliver on our tactical plans. They have the experience and knowledge that elevated our small business digital marketing strategy, increasing our brand awareness and our annual sales through their creative ideas and focused implementation.",
},
{
id: 4,
name: "Jonny Bierman",
company: "Eco Escape Travel",
image:
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/jonny-b-e1535658646997-150x150.jpg-LrFXzlyPhj42YmEsuUjPPu4Jp85K6K.jpeg",
quote:
"Working with Full Circle marries professionalism with real people. These are fantastic folks who are world travelers and also amazing humans. They take the time to understand their clients, identify the challenges, turn those challenges into opportunities, and make magic happen. You'll likely have new friends out of this relationship as they're such genuine and honest people, with great intentions.",
},
{
id: 5,
name: "Sarah Harrill",
company: "Canadian Artist",
image:
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/IMG_1928-e1535559779952-150x150.jpg-MW3VgDI67w6qK0Xb7NlM3NYMA0v0pY.jpeg",
quote:
"Social media marketing is a necessity in growing my community, from projecting a consistent brand voice, to putting out relevant content, and understanding my audience for optimal advertising. Without help from the Full Circle team, I could not fathom doing this on my own. I am an artist, so my business is very personal and it can be difficult for me to promote my work online in an engaging way, so I am lucky to work with such well versed people in this field.",
},
{
id: 6,
name: "Jim Barr",
company: "Seekers Media",
image:
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Jim-Seekers-Media-150x150-qfQrHZDBqNyXKRSYfymt5FyXh5OzgF.png",
quote:
"Professional and able to get sh*t done, not in a months time, but today! The team at Full Circle care about their partners, they take the time required to understand your business and it shows. Our results in working with Full Circle have not only reduced our marketing costs, but have allowed Seekers Media to stretch our marketing dollars further then expected.",
},
]
export function TestimonialSlider() {
const [currentSlide, setCurrentSlide] = useState(0)
const [isDesktop, setIsDesktop] = useState(true)
useEffect(() => {
const handleResize = () => {
setIsDesktop(window.innerWidth >= 768)
}
handleResize()
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}, [])
// Create slide groups - 2 testimonials per slide on desktop, 1 on mobile
const itemsPerSlide = isDesktop ? 2 : 1
const slideGroups = []
for (let i = 0; i < testimonials.length; i += itemsPerSlide) {
slideGroups.push(testimonials.slice(i, i + itemsPerSlide))
}
const totalSlides = slideGroups.length
const nextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % totalSlides)
}
const prevSlide = () => {
setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides)
}
useEffect(() => {
const interval = setInterval(nextSlide, 6000)
return () => clearInterval(interval)
}, [totalSlides])
return (
<div className="relative max-w-6xl mx-auto">
<div className="overflow-hidden">
<div
className="flex transition-transform duration-500 ease-in-out"
style={{
transform: `translateX(-${currentSlide * 100}%)`,
}}
>
{slideGroups.map((group, slideIndex) => (
<div key={slideIndex} className="w-full flex-shrink-0 flex gap-4 px-4">
{group.map((testimonial) => (
<div key={testimonial.id} className={`${isDesktop ? "w-1/2" : "w-full"}`}>
<Card className="bg-white/10 backdrop-blur-sm border-white/20 text-white h-full">
<CardContent className="p-6">
<div className="flex items-center mb-4">
<Image
src={testimonial.image || "/placeholder.svg"}
alt={testimonial.name}
width={60}
height={60}
className="rounded-full mr-4 object-cover"
/>
<div>
<h4 className="testimonial-name font-normal">{testimonial.name}</h4>
<p className="text-sm text-white/80 font-roboto">{testimonial.company}</p>
</div>
</div>
<blockquote className="text-sm leading-relaxed">"{testimonial.quote}"</blockquote>
</CardContent>
</Card>
</div>
))}
</div>
))}
</div>
</div>
<div className="flex justify-center mt-8 space-x-4">
<Button
variant="outline"
size="icon"
onClick={prevSlide}
className="bg-white/10 border-white/20 text-white hover:bg-white/20"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={nextSlide}
className="bg-white/10 border-white/20 text-white hover:bg-white/20"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="flex justify-center mt-4 space-x-2">
{slideGroups.map((_, index) => (
<button
key={index}
onClick={() => setCurrentSlide(index)}
className={`w-2 h-2 rounded-full transition-colors ${index === currentSlide ? "bg-white" : "bg-white/40"}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,11 @@
'use client'
import * as React from 'react'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

59
components/ui/alert.tsx Normal file
View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

View File

@ -0,0 +1,7 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

50
components/ui/avatar.tsx Normal file
View File

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

36
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

View File

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

56
components/ui/button.tsx Normal file
View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

79
components/ui/card.tsx Normal file
View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

262
components/ui/carousel.tsx Normal file
View File

@ -0,0 +1,262 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

365
components/ui/chart.tsx Normal file
View File

@ -0,0 +1,365 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

153
components/ui/command.tsx Normal file
View File

@ -0,0 +1,153 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

122
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

118
components/ui/drawer.tsx Normal file
View File

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

View File

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

178
components/ui/form.tsx Normal file
View File

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@ -0,0 +1,71 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"
import { cn } from "@/lib/utils"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

22
components/ui/input.tsx Normal file
View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"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",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

26
components/ui/label.tsx Normal file
View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

236
components/ui/menubar.tsx Normal file
View File

@ -0,0 +1,236 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@ -0,0 +1,117 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}

31
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,45 @@
"use client"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

Some files were not shown because too many files have changed in this diff Show More