Initial commit: Full Circle Digital Marketing website new
This commit is contained in:
commit
0a022e9dfa
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
Loading…
Reference in New Issue