Compare commits

...

2 Commits

34 changed files with 1396 additions and 389 deletions

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
allowed-build-from-dependencies[]=sharp
allowed-build-from-dependencies[]=unrs-resolver

View File

@@ -1,5 +1,11 @@
export default function Page() { import ContactForm from '@/components/contact-form'
return (
<h1>contact page</h1> export default function ContactPage() {
) return (
<div className="flex min-h-screen flex-col bg-zinc-50 font-sans dark:bg-black">
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col items-center px-8 py-16 sm:py-24">
<ContactForm />
</main>
</div>
)
} }

View File

@@ -19,6 +19,10 @@
} }
} }
html {
scroll-padding-top: 100px;
}
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);

View File

@@ -1,7 +1,8 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Menubar from '../components/menubar'; import SiteHeader from '../components/site-header';
import Footer from "@/components/footer";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -18,6 +19,11 @@ export const metadata: Metadata = {
description: "Generated by create next app", description: "Generated by create next app",
}; };
export const viewport = {
width: "device-width",
initialScale: 1,
};
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
@@ -29,8 +35,9 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
> >
<body className="min-h-full flex flex-col dark:bg-black"> <body className="min-h-full flex flex-col dark:bg-black">
<Menubar /> <SiteHeader />
{children} {children}
<Footer />
</body> </body>
</html> </html>
); );

View File

@@ -1,25 +1,60 @@
import Image from "next/image";
import HeroSection from "@/components/hero-section"; import HeroSection from "@/components/hero-section";
import ContentCard from "@/components/ui/content-cards"; import ContentCardCarousel from "@/components/content-card-carousel";
import Footer from "@/components/footer";
import PicWithText from "@/components/pic-with-text"; import PicWithText from "@/components/pic-with-text";
import pictureOne from "../assets/hero-picture-1.png" import pictureOne from "../assets/hero-picture-1.png"
import pictureTwo from "../assets/hero-picture-2.png" import pictureTwo from "../assets/hero-picture-2.png"
import pictureThree from "../assets/hero-picture-3.png" import pictureThree from "../assets/hero-picture-3.png"
import logo from "../assets/milaoktasafitri-astronomy-9746537_1920.png"
import { title } from "process";
import { reverse } from "dns";
export default function Home() { export default function Home() {
const contentCards = [
{
heading: "Michala Art Shop",
card_text: "This case study was for a small business selling artwork",
button_text: "click me",
src: pictureOne.src
},
{
heading: "Athlito",
card_text: "This was an Ecommerce padel shop",
button_text: "click me",
src: pictureTwo.src
},
{
heading: "Birchover Reclamation",
card_text: "This case study was for a stone yard company",
button_text: "click me",
src: pictureThree.src
},
{
heading: "Michala Art Shop",
card_text: "This case study was for a small business selling artwork",
button_text: "click me",
src: pictureOne.src
}
]
const picWithText = {
title: "Building a better web, one pixel at a time",
text: "At Sculpted Arc, we are passionate about crafting exceptional web solutions that empower businesses to thrive in the digital landscape. With a team of skilled developers and designers, we specialize in creating stunning websites that not only captivate audiences but also drive results. Whether you're a startup looking to establish your online presence or an established company seeking to revamp your website, we have the expertise to bring your vision to life. Our commitment to quality, innovation, and customer satisfaction sets us apart as a trusted partner in your web development journey.",
src: logo.src,
reverse: false
}
return ( return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="flex min-h-screen flex-col bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-7xl flex-col items-center justify-between py-32 px-8 bg-white dark:bg-black sm:items-start"> <main className="mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-between bg-white px-8 py-32 dark:bg-black sm:items-start">
<HeroSection /> <HeroSection />
<div className="flex flex-row flex-wrap justify-center m-auto"> <PicWithText {...picWithText} />
<ContentCard heading="Michala Art Shop" card_text="This case study was for a small business selling artwork" button_text="click me" src={pictureOne.src}/>
<ContentCard heading="Athlito" card_text="This was an Ecommerce padel shop" button_text="click me" src={pictureTwo.src}/> <div className="w-full m-auto">
<ContentCard heading="Birchover Reclamation" card_text="This case study was for a stone yard company" button_text="click me" src={pictureThree.src}/> <ContentCardCarousel cards={contentCards} />
<ContentCard heading="Michala Art Shop" card_text="This case study was for a small business selling artwork" button_text="click me" src={pictureOne.src}/>
</div> </div>
<PicWithText />
</main> </main>
</div> </div>
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/saws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/scws-logo-no-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,252 +0,0 @@
/* app/Menubar.css */
.menubar {
position: sticky;
top: 20px;
z-index: 1000;
display: flex;
justify-content: center;
align-self: center;
width: calc(100% - 40px);
min-width: 720px;
max-width: 1000px;
margin: 20px auto 0;
border-radius: 30px;
background-color: rgba(29, 29, 29, 0.9);
backdrop-filter: blur(10px);
padding: 10px 20px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(60, 60, 60, 0.3);
isolation: isolate;
transition: padding 0.35s ease;
}
.menubar::before {
content: '';
position: absolute;
inset: -5px;
background: linear-gradient(45deg, #ff00ff, #00ffff);
filter: blur(25px);
border-radius: 20px;
z-index: 0;
opacity: 0.6;
pointer-events: none;
}
.menubar-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
width: 100%;
max-width: 960px;
transition: flex-direction 0.35s ease;
}
.desktop-menu {
display: flex;
gap: 30px;
}
.menu-item {
color: #e0e0e0;
text-decoration: none;
font-size: 1.1em;
padding: 5px 10px;
transition: color 0.3s ease, transform 0.3s ease;
}
.menu-item:hover {
color: #ffffff;
transform: translateY(-2px);
}
.mobile-menu {
display: none;
width: 100%;
position: relative;
}
.mobile-menu-checkbox {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.burger-icon {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: #e0e0e0;
font-size: 2em;
line-height: 1;
padding: 5px 10px;
position: relative;
z-index: 3;
touch-action: manipulation;
width: 1.4em;
height: 1.4em;
cursor: pointer;
}
.burger-icon-menu,
.burger-icon-close {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.burger-icon-menu {
opacity: 1;
transform: scale(1) rotate(0deg);
}
.burger-icon-close {
opacity: 0;
transform: scale(0.8) rotate(-90deg);
}
.mobile-menu-checkbox:checked + .burger-icon .burger-icon-menu {
opacity: 0;
transform: scale(0.8) rotate(90deg);
}
.mobile-menu-checkbox:checked + .burger-icon .burger-icon-close {
opacity: 1;
transform: scale(1) rotate(0deg);
}
.mobile-dropdown {
display: grid;
grid-template-rows: 0fr;
width: 100%;
opacity: 0;
margin-top: 0;
padding-top: 0;
border-top: 1px solid transparent;
pointer-events: none;
transition:
grid-template-rows 0.35s ease,
opacity 0.35s ease,
margin-top 0.35s ease,
padding-top 0.35s ease,
border-color 0.35s ease;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown {
grid-template-rows: 1fr;
opacity: 1;
margin-top: 8px;
padding-top: 12px;
border-top-color: rgba(60, 60, 60, 0.3);
pointer-events: auto;
}
.mobile-dropdown-inner {
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
min-height: 0;
}
.mobile-dropdown-item {
display: block;
width: 100%;
padding: 12px 10px;
border-radius: 16px;
text-align: left;
opacity: 0;
transform: translateY(-6px);
transition:
opacity 0.3s ease,
transform 0.3s ease,
background-color 0.3s ease;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item {
opacity: 1;
transform: translateY(0);
}
.mobile-dropdown-item:hover {
background-color: rgba(60, 60, 60, 0.4);
transform: translateY(0);
}
/* Responsive design */
@media (max-width: 750px) {
.menubar {
min-width: 0;
width: calc(100% - 24px);
margin-top: 16px;
justify-content: flex-start;
}
.desktop-menu {
display: none;
}
.mobile-menu {
display: block;
}
.menubar-content {
justify-content: flex-start;
align-items: stretch;
}
.menubar:has(.mobile-menu-checkbox:checked) .menubar-content {
flex-direction: column;
align-items: stretch;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(1) {
transition-delay: 0.05s;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(2) {
transition-delay: 0.1s;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(3) {
transition-delay: 0.15s;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(4) {
transition-delay: 0.2s;
}
}
@media (prefers-reduced-motion: reduce) {
.menubar,
.menubar-content,
.burger-icon-menu,
.burger-icon-close,
.mobile-dropdown,
.mobile-dropdown-item {
transition: none;
}
.mobile-menu-checkbox:checked + .burger-icon .burger-icon-menu,
.burger-icon-close {
transform: none;
}
.mobile-dropdown-item {
opacity: 1;
transform: none;
}
}

167
components/contact-form.css Normal file
View File

@@ -0,0 +1,167 @@
.contact-form-box {
position: relative;
isolation: isolate;
width: 100%;
max-width: 36rem;
margin: 0 auto;
padding: 2rem;
border-radius: 1.5rem;
background-color: rgba(255, 255, 255, 0.95);
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.contact-form-box::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(
135deg,
rgba(56, 189, 248, 0.45),
rgba(109, 40, 217, 0.35),
rgba(60, 60, 60, 0.15)
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
}
.contact-form-inner {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.contact-form-title {
margin: 0 0 0.25rem;
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(90deg, #38bdf8, #6d28d9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
.contact-form-description {
margin: 0 0 0.5rem;
font-size: 0.95rem;
line-height: 1.5;
color: #6b7280;
}
.contact-form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.contact-form-label {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
}
.contact-form-label-optional {
font-weight: 400;
color: #9ca3af;
}
.contact-form-input,
.contact-form-textarea {
width: 100%;
border-radius: 0.75rem;
border: 1px solid rgba(60, 60, 60, 0.25);
background-color: rgba(255, 255, 255, 0.9);
padding: 0.75rem 1rem;
font-size: 1rem;
line-height: 1.5;
color: #171717;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.contact-form-input::placeholder,
.contact-form-textarea::placeholder {
color: #9ca3af;
}
.contact-form-input:focus,
.contact-form-textarea:focus {
outline: none;
border-color: #38bdf8;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
}
.contact-form-textarea {
min-height: 10rem;
resize: vertical;
}
.contact-form-submit {
align-self: flex-start;
margin-top: 0.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.625rem 1.5rem;
border: none;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 600;
color: #fff;
background: linear-gradient(90deg, #38bdf8, #6d28d9);
box-shadow: 0 2px 12px rgba(56, 189, 248, 0.25);
cursor: pointer;
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
filter 0.25s ease;
}
.contact-form-submit:hover {
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(109, 40, 217, 0.35);
filter: brightness(1.08);
}
.contact-form-submit:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.contact-form-box {
background-color: rgba(24, 24, 27, 0.95);
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.contact-form-description {
color: #a1a1aa;
}
.contact-form-label {
color: #e4e4e7;
}
.contact-form-input,
.contact-form-textarea {
border-color: rgba(255, 255, 255, 0.15);
background-color: rgba(0, 0, 0, 0.25);
color: #fafafa;
}
}

View File

@@ -0,0 +1,73 @@
'use client'
import { type FormEvent, useState } from 'react'
import './contact-form.css'
export default function ContactForm() {
const [submitted, setSubmitted] = useState(false)
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setSubmitted(true)
}
return (
<div className="contact-form-box">
<form className="contact-form-inner" onSubmit={handleSubmit} noValidate>
<div>
<h2 className="contact-form-title">Get in touch</h2>
<p className="contact-form-description">
Send us a message and we&apos;ll get back to you as soon as we can.
</p>
</div>
<div className="contact-form-field">
<label htmlFor="contact-email" className="contact-form-label">
Email
</label>
<input
id="contact-email"
name="email"
type="email"
required
autoComplete="email"
placeholder="you@example.com"
className="contact-form-input"
/>
</div>
<div className="contact-form-field">
<label htmlFor="contact-phone" className="contact-form-label">
Phone <span className="contact-form-label-optional">(optional)</span>
</label>
<input
id="contact-phone"
name="phone"
type="tel"
autoComplete="tel"
placeholder="+44 7700 900000"
className="contact-form-input"
/>
</div>
<div className="contact-form-field">
<label htmlFor="contact-message" className="contact-form-label">
Message
</label>
<textarea
id="contact-message"
name="message"
required
rows={6}
placeholder="Tell us about your project..."
className="contact-form-textarea"
/>
</div>
<button type="submit" className="contact-form-submit">
{submitted ? 'Message sent' : 'Send message'}
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,62 @@
/* Slide width is driven by CSS breakpoints (not JS innerWidth) so real phones match devtools */
.carousel-viewport {
width: 100%;
}
.carousel-slide {
flex: 0 0 100%;
min-width: 0;
box-sizing: border-box;
}
@media (min-width: 640px) {
.carousel-slide {
flex: 0 0 50%;
}
}
@media (min-width: 1024px) {
.carousel-slide {
flex: 0 0 33.333%;
}
}
.carousel-controls {
position: relative;
z-index: 20;
isolation: isolate;
}
.carousel-control-btn {
display: inline-flex;
min-height: 44px;
min-width: 44px;
align-items: center;
justify-content: center;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
border-radius: 9999px;
border: 1px solid var(--color-default-medium, rgba(60, 60, 60, 0.35));
background-color: var(--color-neutral-secondary-medium, rgba(60, 60, 60, 0.25));
padding: 0.5rem 1rem;
font-size: 1.125rem;
line-height: 1;
color: inherit;
transition: background-color 0.2s ease, color 0.2s ease;
}
.carousel-control-btn:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.9);
color: #000;
}
.carousel-control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.carousel-control-btn:active:not(:disabled) {
transform: scale(0.96);
}

View File

@@ -0,0 +1,126 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import ContentCard from './ui/content-cards'
import './content-card-carousel.css'
interface ContentCardItem {
src?: string
button_link?: string
heading?: string
button_text?: string
card_text?: string
}
interface ContentCardCarouselProps {
cards: ContentCardItem[]
}
function getVisibleCardCount() {
if (typeof window === 'undefined') return 1
if (window.matchMedia('(min-width: 1024px)').matches) return 3
if (window.matchMedia('(min-width: 640px)').matches) return 2
return 1
}
export default function ContentCardCarousel({ cards }: ContentCardCarouselProps) {
const [visibleCards, setVisibleCards] = useState(1)
const [currentIndex, setCurrentIndex] = useState(0)
const syncVisibleCards = useCallback(() => {
setVisibleCards(getVisibleCardCount())
}, [])
useEffect(() => {
syncVisibleCards()
const mediaQueries = [
window.matchMedia('(min-width: 640px)'),
window.matchMedia('(min-width: 1024px)'),
]
const onBreakpointChange = () => syncVisibleCards()
mediaQueries.forEach((mq) => mq.addEventListener('change', onBreakpointChange))
window.addEventListener('resize', onBreakpointChange)
return () => {
mediaQueries.forEach((mq) => mq.removeEventListener('change', onBreakpointChange))
window.removeEventListener('resize', onBreakpointChange)
}
}, [syncVisibleCards])
const maxIndex = useMemo(() => Math.max(cards.length - visibleCards, 0), [cards.length, visibleCards])
useEffect(() => {
setCurrentIndex((previous) => Math.min(previous, maxIndex))
}, [maxIndex])
// % of track width — matches flex slide sizes (100% / 50% / 33.333% per breakpoint)
const trackOffsetPercent = cards.length > 0 ? (currentIndex * 100) / cards.length : 0
const next = () => {
if (maxIndex === 0) return
setCurrentIndex((previous) => (previous >= maxIndex ? 0 : previous + 1))
}
const previous = () => {
if (maxIndex === 0) return
setCurrentIndex((index) => (index <= 0 ? maxIndex : index - 1))
}
return (
<div className="w-full max-w-full pb-30">
<h1 className="mb-4 flex justify-center text-5xl font-bold">Our Case Studies</h1>
<div className="carousel-viewport relative overflow-hidden">
<div
className="carousel-track flex items-stretch transition-transform duration-500 ease-in-out"
style={{ transform: `translate3d(-${trackOffsetPercent}%, 0, 0)` }}
>
{cards.map((card, index) => {
const isEdgeCard =
visibleCards > 1 && (index === currentIndex || index === currentIndex + visibleCards - 1)
return (
<div
key={`${card.heading ?? 'card'}-${index}`}
className={`carousel-slide flex shrink-0 justify-center px-2 py-3 transition-opacity duration-500 ${
isEdgeCard ? 'opacity-75' : 'opacity-100'
}`}
>
<div className="flex h-full w-full max-w-sm">
<ContentCard {...card} />
</div>
</div>
)
})}
</div>
<div className="pointer-events-none absolute inset-y-0 left-0 w-12 bg-gradient-to-r from-white to-transparent dark:from-black" />
<div className="pointer-events-none absolute inset-y-0 right-0 w-12 bg-gradient-to-l from-white to-transparent dark:from-black" />
</div>
<div className="carousel-controls relative z-20 mt-6 flex justify-center gap-4">
<button
type="button"
onClick={previous}
disabled={maxIndex === 0}
className="carousel-control-btn"
aria-label="Show previous content cards"
>
&#8592;
</button>
<button
type="button"
onClick={next}
disabled={maxIndex === 0}
className="carousel-control-btn"
aria-label="Show next content cards"
>
&#8594;
</button>
</div>
</div>
)
}

22
components/footer.tsx Normal file
View File

@@ -0,0 +1,22 @@
import saws from "../assets/saws.png"
import SocialMediaLinks from "./social-media-links"
export default function Footer() {
return (
<footer className="w-full border-t border-neutral-300 bg-white dark:border-neutral-800 dark:bg-black">
<div className="mx-auto flex w-full max-w-7xl flex-col items-center justify-between gap-4 px-8 py-6 text-sm text-body md:flex-row">
<div className="flex items-center gap-3">
<img src={saws.src} alt="Sculpted Arc logo" className="h-12 w-auto" />
<p className="text-center md:text-left">copyright 2026 sculpted arc</p>
</div>
<div className="flex items-center gap-5">
<a href="/privacy-policy" className="hover:text-heading transition-colors">
Privacy Policy
</a>
<SocialMediaLinks linkClassName="hover:text-heading transition-colors" />
</div>
</div>
</footer>
)
}

View File

@@ -3,6 +3,7 @@
.hero-main { .hero-main {
width: 100%; width: 100%;
min-height: 400px; min-height: 400px;
padding-bottom: 200px;
} }
.hero-picture-stack { .hero-picture-stack {
@@ -38,19 +39,9 @@
} }
@media (max-width: 750px) { @media (max-width: 750px) {
.hero-picture-stack { .herp-main {
width: 100%; width: 100%;
min-height: 260px; min-height: 260px;
} }
.hero-picture {
width: 75%;
max-width: 240px;
}
.hero-right {
width: 100%;
padding-left: 0;
padding-top: 60px;
}
} }

View File

@@ -1,21 +1,25 @@
import ContentCard from "./ui/content-cards"; import ContentCard from "./ui/content-cards";
import './hero-section.css' import './hero-section.css'
import heroPictureOne from "../assets/hero-picture-1.png" import heroPictureOne from "../assets/mxj-files-planet-27066.gif"
import heroPictureTwo from "../assets/hero-picture-2.png"
import heroPictureThree from "../assets/hero-picture-3.png"
export default function HeroSection() { export default function HeroSection() {
return( return(
<> <>
<div className="hero-main flex flex-wrap"> <div className="flex flex-wrap-reverse justify-items-center ">
<div className="hero-picture-stack p-4"> <div className="p-4 w-full md:w-1/2 shrink-0">
<img className="hero-picture" src={heroPictureOne.src} alt="" /> <img className="w-full h-auto rounded-2xl object-cover" src={heroPictureOne.src} alt="" />
<img className="hero-picture hero-picture--tilt-20" src={heroPictureTwo.src} alt="" />
<img className="hero-picture hero-picture--tilt-40" src={heroPictureThree.src} alt="" />
</div> </div>
<div className="hero-right"> <div className="w-full md:w-1/2">
<h1 className="mb-4 text-5xl font-bold text-heading md:text-6xl lg:text-7xl"><span className="text-transparent bg-clip-text bg-gradient-to-r bg-violet-800 from-sky-400">Sculpted Arc</span><br/> Web Solutions</h1> <h1 className="mb-4 font-bold text-heading text-6xl sm:text-7xl"><span className="text-transparent bg-clip-text bg-gradient-to-r bg-violet-800 from-sky-400">Sculpted Arc</span><br/> Web Solutions</h1>
<hr className="w-1/4 mb-7 mt-8 border-gray-700 border-2 rounded-2xl"/>
<p className="text-lg font-normal text-body lg:text-xl">Here at Sculpted Arc we are invested in creating great website and solving complex web problems Here at Sculpted Arc we are invested in creating great website and solving complex web problems</p> <p className="text-lg font-normal text-body lg:text-xl">Here at Sculpted Arc we are invested in creating great website and solving complex web problems Here at Sculpted Arc we are invested in creating great website and solving complex web problems</p>
<br/>
<a href="#" className="transition duration-150 flex w-55 justify-start rounded-3xl text-body bg-neutral-secondary-medium box-border border border-default-medium hover:bg-blue-200 hover:text-black hover:text-heading focus:ring-4 focus:ring-neutral-tertiary shadow-xs font-medium leading-5 rounded-base text-sm px-4 py-2.5 focus:outline-none mt-auto">
Tell us about your project
<svg className="w-3 h-3 ms-1.5 rtl:rotate-180 -me-0.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 12H5m14 0-4 4m4-4-4-4"/></svg>
</a>
</div> </div>
</div> </div>
</> </>

View File

@@ -1,74 +0,0 @@
'use client';
import { useRef, type MouseEvent } from 'react';
import './Menubar.css';
const menuLinks = [
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about-us' },
{ label: 'Plans', href: '/plans' },
{ label: 'Contact', href: '#' },
];
export default function Menubar() {
const menuToggleRef = useRef<HTMLInputElement>(null);
const closeMobileMenu = () => {
if (menuToggleRef.current) {
menuToggleRef.current.checked = false;
}
};
const handleLinkClick = (event: MouseEvent<HTMLAnchorElement>) => {
closeMobileMenu();
event.currentTarget.blur();
};
return (
<nav className="menubar">
<div className="menubar-content">
<div className="desktop-menu">
{menuLinks.map((link) => (
<a key={link.label} href={link.href} className="menu-item">
{link.label}
</a>
))}
</div>
<div className="mobile-menu">
<input
ref={menuToggleRef}
type="checkbox"
id="mobile-nav-toggle"
className="mobile-menu-checkbox"
/>
<label
htmlFor="mobile-nav-toggle"
className="burger-icon"
aria-controls="mobile-nav-menu"
>
<span className="burger-icon-menu" aria-hidden="true">
&#9776;
</span>
<span className="burger-icon-close" aria-hidden="true">
&times;
</span>
</label>
<div id="mobile-nav-menu" className="mobile-dropdown">
<div className="mobile-dropdown-inner">
{menuLinks.map((link) => (
<a
key={link.label}
href={link.href}
className="menu-item mobile-dropdown-item"
onClick={handleLinkClick}
>
{link.label}
</a>
))}
</div>
</div>
</div>
</div>
</nav>
);
}

View File

@@ -1,18 +1,29 @@
import picOne from "../assets/hero-picture-1.png"; import picOne from "../assets/hero-picture-1.png";
export default function PicWithText(){
interface PicWithTextProps {
src?: string
title?: string
text?: string
reverse: boolean
}
export default function PicWithText(content: PicWithTextProps){
let flex_state = content.reverse ? "md:flex-row flex-col" : "md:flex-row-reverse flex-col-reverse";
return ( return (
<div className="flex flex-col md:flex-row items-center md:items-start gap-8 w-full mt-10"> <div className={`flex ${flex_state} items-center md:items-start gap-8 w-full mt-10 pb-30`}>
<div className="w-full md:w-1/2 shrink-0"> <div className="w-full md:w-1/2 shrink-0">
<img <img
className="w-full h-auto rounded-2xl object-cover" className="w-full h-auto rounded-2xl object-cover"
src={picOne.src} src={content.src}
alt="" alt=""
/> />
</div> </div>
<div className="w-full md:w-1/2"> <div className="w-full md:w-1/2">
<h1 className="mb-4 text-5xl font-bold">Title of this section</h1> <h1 className="mb-4 text-5xl font-bold">{content.title}</h1>
<p className="text-lg font-normal text-body lg:text-xl">This is the paragraph for this section. This is also just random filler text to see how things look. here are some extra words htat make no sence!</p> <p className="text-lg font-normal text-body lg:text-xl">{content.text}</p>
<div className="flex items-center w-full my-8"> <div className="flex items-center w-full my-8">
<div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" /> <div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" />
<div className="px-3"> <div className="px-3">
@@ -20,7 +31,7 @@ export default function PicWithText(){
</div> </div>
<div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" /> <div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" />
</div> </div>
<p>This is more text, however, this is actually a quote, yes, a quote indeed!</p> <p className="text-gray-300 italic">This is more text, however, this is actually a quote, yes, a quote indeed!</p>
</div> </div>
</div> </div>
) )

463
components/site-header.css Normal file
View File

@@ -0,0 +1,463 @@
.site-header-wrapper {
position: sticky;
top: 0;
z-index: 1000;
display: flex;
justify-content: center;
width: 100%;
padding: 16px 20px 0;
pointer-events: none;
}
.site-header {
position: relative;
pointer-events: auto;
display: flex;
justify-content: center;
width: calc(100% - 8px);
min-width: 0;
max-width: 1120px;
margin: 0 auto;
border-radius: 30px;
padding: 12px 20px;
background-color: rgba(0, 0, 0, 0.88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow:
0 4px 30px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
isolation: isolate;
transition:
padding 0.35s ease,
border-radius 0.35s ease,
box-shadow 0.35s ease,
backdrop-filter 0.35s ease,
background-color 0.35s ease;
}
.site-header::before {
content: '';
position: absolute;
inset: -5px;
background: black;
filter: blur(25px);
border-radius: 20px;
z-index: -1;
opacity: 0.12;
pointer-events: none;
}
.site-header::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(
135deg,
rgba(56, 189, 248, 0.5),
rgba(91, 33, 182, 0.4),
rgba(114, 8, 156, 0.5)
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
}
.site-header--scrolled {
padding: 8px 16px;
border-radius: 24px;
background-color: rgba(20, 18, 18, 0.92);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.28),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.site-header--scrolled::before {
opacity: 0.18;
}
.site-header-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 16px;
width: 100%;
}
.site-header-logo {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
text-decoration: none;
color: #e0e0e0;
transition: opacity 0.25s ease;
}
.site-header-logo:hover {
opacity: 0.9;
}
.site-header-logo-img {
height: 36px;
width: auto;
transition: height 0.35s ease;
}
.site-header--scrolled .site-header-logo-img {
height: 30px;
}
.site-header-logo-text {
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.02em;
background: linear-gradient(90deg, #38bdf8, #6d28d9);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.site-header-nav {
display: flex;
flex: 1;
justify-content: center;
gap: 8px;
}
.menu-item {
color: #c8c8c8;
text-decoration: none;
font-size: 0.95rem;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 8px 14px;
border-radius: 9999px;
/* Only animate transform — color/background transitions flash when active class toggles */
transition: transform 0.25s ease;
}
.menu-item:hover {
color: #e0e0e0;
background-color: rgba(66, 116, 255, 0.12);
transform: translateY(-1px);
}
.menu-item.menu-item--active,
.menu-item.menu-item--active:hover {
background-color: rgba(56, 189, 248, 0.12);
background-image: linear-gradient(90deg, #38bdf8, #a78bfa);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
transform: none;
transition: none;
}
.site-header-actions {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.site-header-social {
color: #c8c8c8;
gap: 12px;
}
.site-header-social-link {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px;
border-radius: 9999px;
transition:
color 0.25s ease,
background-color 0.25s ease,
transform 0.25s ease;
}
.site-header-social-link:hover {
color: #38bdf8;
background-color: rgba(56, 189, 248, 0.12);
transform: translateY(-1px);
}
.site-header-cta {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 8px 18px;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
color: #fff;
background: linear-gradient(90deg, #38bdf8, #6d28d9);
box-shadow: 0 2px 12px rgba(56, 189, 248, 0.25);
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
filter 0.25s ease;
}
.site-header-cta:hover {
transform: translateY(-1px);
box-shadow: 0 10px 20px rgba(109, 40, 217, 0.35);
filter: brightness(1.08);
}
.site-header-cta:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}
.site-header-cta--mobile {
margin-top: 8px;
width: 100%;
text-align: center;
}
.site-header-social--mobile {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(60, 60, 60, 0.35);
justify-content: center;
}
.mobile-menu {
display: none;
margin-left: auto;
position: relative;
}
.mobile-menu-checkbox {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.burger-icon {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
color: #e0e0e0;
font-size: 2em;
line-height: 1;
padding: 5px 10px;
position: relative;
z-index: 3;
touch-action: manipulation;
width: 1.4em;
height: 1.4em;
cursor: pointer;
border-radius: 12px;
transition: background-color 0.25s ease;
}
.burger-icon:hover {
background-color: rgba(66, 116, 255, 0.12);
}
.burger-icon-menu,
.burger-icon-close {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.burger-icon-menu {
opacity: 1;
transform: scale(1) rotate(0deg);
}
.burger-icon-close {
opacity: 0;
transform: scale(0.8) rotate(-90deg);
}
.mobile-menu-checkbox:checked + .burger-icon .burger-icon-menu {
opacity: 0;
transform: scale(0.8) rotate(90deg);
}
.mobile-menu-checkbox:checked + .burger-icon .burger-icon-close {
opacity: 1;
transform: scale(1) rotate(0deg);
}
.mobile-dropdown {
display: grid;
grid-template-rows: 0fr;
position: absolute;
top: 100%;
left: 0;
right: 0;
width: 100%;
opacity: 0;
margin-top: 0;
padding-top: 0;
border-top: 1px solid transparent;
pointer-events: none;
transition:
grid-template-rows 0.35s ease,
opacity 0.35s ease,
margin-top 0.35s ease,
padding-top 0.35s ease,
border-color 0.35s ease;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown {
grid-template-rows: 1fr;
opacity: 1;
margin-top: 8px;
padding-top: 12px;
border-top-color: rgba(60, 60, 60, 0.3);
pointer-events: auto;
}
.mobile-dropdown-inner {
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
min-height: 0;
}
.mobile-dropdown-item {
display: block;
width: 100%;
text-align: left;
opacity: 0;
transform: translateY(-6px);
transition:
opacity 0.3s ease,
transform 0.3s ease,
background-color 0.3s ease;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item,
.mobile-menu-checkbox:checked ~ .mobile-dropdown .site-header-cta--mobile,
.mobile-menu-checkbox:checked ~ .mobile-dropdown .site-header-social--mobile {
opacity: 1;
transform: translateY(0);
}
.mobile-dropdown-item:hover {
background-color: rgba(60, 60, 60, 0.4);
transform: translateY(0);
}
@media (max-width: 900px) {
.site-header-logo-text {
display: none;
}
}
@media (max-width: 800px) {
.site-header-wrapper {
padding: 12px 12px 0;
}
.site-header {
width: 100%;
padding: 10px 14px;
}
.site-header--scrolled {
padding: 8px 12px;
}
.desktop-menu,
.desktop-actions {
display: none;
}
.mobile-menu {
display: block;
}
.site-header-content {
flex-wrap: wrap;
}
.site-header:has(.mobile-menu-checkbox:checked) .site-header-content {
align-items: stretch;
}
.site-header:has(.mobile-menu-checkbox:checked) .mobile-menu {
width: 100%;
order: 10;
}
.mobile-dropdown {
position: static;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(1) {
transition-delay: 0.05s;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(2) {
transition-delay: 0.1s;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(3) {
transition-delay: 0.15s;
}
.mobile-menu-checkbox:checked ~ .mobile-dropdown .mobile-dropdown-item:nth-child(4) {
transition-delay: 0.2s;
}
}
@media (prefers-reduced-motion: reduce) {
.site-header,
.site-header-logo-img,
.burger-icon-menu,
.burger-icon-close,
.mobile-dropdown,
.mobile-dropdown-item,
.menu-item,
.site-header-cta,
.site-header-social-link {
transition: none;
}
.mobile-menu-checkbox:checked + .burger-icon .burger-icon-menu,
.burger-icon-close {
transform: none;
}
.mobile-dropdown-item,
.site-header-cta--mobile,
.site-header-social--mobile {
opacity: 1;
transform: none;
}
}

147
components/site-header.tsx Normal file
View File

@@ -0,0 +1,147 @@
'use client'
import { useEffect, useRef, useState, type MouseEvent } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import saws from '../assets/saws.png'
import SocialMediaLinks from './social-media-links'
import './site-header.css'
const menuLinks = [
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about-us' },
{ label: 'Plans', href: '/plans' },
{ label: 'Contact', href: '/contact' },
] as const
const ctaHref = '/contact'
function isActivePath(pathname: string, href: string) {
if (href === '/') return pathname === '/'
return pathname === href || pathname.startsWith(`${href}/`)
}
export default function SiteHeader() {
const pathname = usePathname()
const menuToggleRef = useRef<HTMLInputElement>(null)
const [scrolled, setScrolled] = useState(false)
const [pendingPath, setPendingPath] = useState<string | null>(null)
const activePath = pendingPath ?? pathname
useEffect(() => {
setPendingPath(null)
}, [pathname])
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
const closeMobileMenu = () => {
if (menuToggleRef.current) {
menuToggleRef.current.checked = false
}
}
const handleNavClick =
(href: string) => (event: MouseEvent<HTMLAnchorElement>) => {
setPendingPath(href)
closeMobileMenu()
event.currentTarget.blur()
}
return (
<header className="site-header-wrapper">
<nav
className={`site-header${scrolled ? ' site-header--scrolled' : ''}`}
aria-label="Main navigation"
>
<div className="site-header-content">
<Link href="/" className="site-header-logo" onClick={handleNavClick('/')}>
<img src={saws.src} alt="Sculpted Arc" className="site-header-logo-img" />
</Link>
<div className="site-header-nav desktop-menu">
{menuLinks.map((link) => {
const active = isActivePath(activePath, link.href)
return (
<Link
key={link.label}
href={link.href}
className={`menu-item${active ? ' menu-item--active' : ''}`}
aria-current={active ? 'page' : undefined}
onClick={handleNavClick(link.href)}
>
{link.label}
</Link>
)
})}
</div>
<div className="site-header-actions desktop-actions">
<SocialMediaLinks
className="site-header-social"
linkClassName="site-header-social-link"
/>
<Link href={ctaHref} className="site-header-cta">
Get a quote
</Link>
</div>
<div className="mobile-menu">
<input
ref={menuToggleRef}
type="checkbox"
id="mobile-nav-toggle"
className="mobile-menu-checkbox"
/>
<label
htmlFor="mobile-nav-toggle"
className="burger-icon"
aria-controls="mobile-nav-menu"
>
<span className="burger-icon-menu" aria-hidden="true">
&#9776;
</span>
<span className="burger-icon-close" aria-hidden="true">
&times;
</span>
</label>
<div id="mobile-nav-menu" className="mobile-dropdown">
<div className="mobile-dropdown-inner">
{menuLinks.map((link) => {
const active = isActivePath(activePath, link.href)
return (
<Link
key={link.label}
href={link.href}
className={`menu-item mobile-dropdown-item${active ? ' menu-item--active' : ''}`}
aria-current={active ? 'page' : undefined}
onClick={handleNavClick(link.href)}
>
{link.label}
</Link>
)
})}
<Link
href={ctaHref}
className="site-header-cta site-header-cta--mobile"
onClick={handleNavClick(ctaHref)}
>
Get a quote
</Link>
<SocialMediaLinks
className="site-header-social site-header-social--mobile"
linkClassName="site-header-social-link"
/>
</div>
</div>
</div>
</div>
</nav>
</header>
)
}

View File

@@ -0,0 +1,54 @@
const socialLinks = [
{
href: 'https://www.instagram.com',
label: 'Instagram',
icon: (
<path d="M7.75 2h8.5A5.75 5.75 0 0 1 22 7.75v8.5A5.75 5.75 0 0 1 16.25 22h-8.5A5.75 5.75 0 0 1 2 16.25v-8.5A5.75 5.75 0 0 1 7.75 2Zm0 1.5A4.25 4.25 0 0 0 3.5 7.75v8.5A4.25 4.25 0 0 0 7.75 20.5h8.5a4.25 4.25 0 0 0 4.25-4.25v-8.5a4.25 4.25 0 0 0-4.25-4.25h-8.5Zm9.5 1.75a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5ZM12 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" />
),
},
{
href: 'https://www.linkedin.com',
label: 'LinkedIn',
icon: (
<path d="M6.94 8.5A1.56 1.56 0 1 1 6.94 5.4a1.56 1.56 0 0 1 0 3.1ZM5.5 10h2.88v8.5H5.5V10Zm4.63 0H12.9v1.16h.04c.39-.74 1.35-1.52 2.79-1.52 2.99 0 3.54 1.97 3.54 4.53v4.33h-2.88V14.66c0-.91-.02-2.07-1.26-2.07-1.27 0-1.46.99-1.46 2.01v3.9h-2.88V10Z" />
),
},
{
href: 'https://x.com',
label: 'X',
icon: (
<path d="M18.9 2H22l-6.77 7.74L23 22h-6.1l-4.78-6.26L6.63 22H3.5l7.25-8.28L1 2h6.26l4.32 5.7L18.9 2Zm-1.07 18h1.69L6.34 3.9H4.53L17.83 20Z" />
),
},
] as const
interface SocialMediaLinksProps {
className?: string
iconClassName?: string
linkClassName?: string
}
export default function SocialMediaLinks({
className = '',
iconClassName = 'h-5 w-5',
linkClassName = 'transition-colors hover:text-[#4274ff]',
}: SocialMediaLinksProps) {
return (
<div className={`flex items-center gap-4 ${className}`.trim()}>
{socialLinks.map((link) => (
<a
key={link.label}
href={link.href}
className={linkClassName}
aria-label={link.label}
target="_blank"
rel="noopener noreferrer"
>
<svg className={iconClassName} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
{link.icon}
</svg>
</a>
))}
</div>
)
}

View File

@@ -0,0 +1,137 @@
.content-card {
position: relative;
isolation: isolate;
border-radius: 1.5rem;
background-color: rgba(255, 255, 255, 0.95);
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
transition:
transform 0.25s ease,
box-shadow 0.25s ease;
}
.content-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(
135deg,
rgba(56, 189, 248, 0.45),
rgba(109, 40, 217, 0.35),
rgba(60, 60, 60, 0.15)
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
transition: opacity 0.25s ease;
}
.content-card:hover {
transform: translateY(-2px);
box-shadow:
0 12px 32px rgba(56, 189, 248, 0.12),
0 8px 24px rgba(109, 40, 217, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.content-card:hover::after {
opacity: 1;
background: linear-gradient(
135deg,
rgba(56, 189, 248, 0.65),
rgba(109, 40, 217, 0.55),
rgba(60, 60, 60, 0.2)
);
}
.content-card-body {
position: relative;
z-index: 1;
}
.content-card-image {
overflow: hidden;
border-radius: 1rem;
background-color: rgba(0, 0, 0, 0.04);
}
.content-card-image img {
transition: transform 0.35s ease;
}
.content-card:hover .content-card-image img {
transform: scale(1.05);
}
.content-card-cta {
display: inline-flex;
align-items: center;
border-radius: 9999px;
padding: 0.625rem 1rem;
font-size: 0.875rem;
font-weight: 600;
line-height: 1.25rem;
text-decoration: none;
color: #fff;
background: linear-gradient(90deg, #38bdf8, #6d28d9);
box-shadow: 0 2px 12px rgba(56, 189, 248, 0.25);
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
filter 0.25s ease;
}
.content-card-cta:hover {
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(109, 40, 217, 0.35);
filter: brightness(1.08);
}
.content-card-cta:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.content-card {
background-color: rgba(24, 24, 27, 0.95);
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.content-card:hover {
box-shadow:
0 12px 32px rgba(56, 189, 248, 0.15),
0 8px 24px rgba(109, 40, 217, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.content-card-image {
background-color: rgba(255, 255, 255, 0.04);
}
}
@media (prefers-reduced-motion: reduce) {
.content-card,
.content-card::after,
.content-card-image img,
.content-card-cta {
transition: none;
}
.content-card:hover,
.content-card:hover .content-card-image img,
.content-card-cta:hover {
transform: none;
}
}

View File

@@ -1,3 +1,4 @@
import './content-cards.css'
interface cardProps { interface cardProps {
src?: string src?: string
@@ -8,21 +9,45 @@ interface cardProps {
} }
export default function ContentCard(CardProps: cardProps) { export default function ContentCard(CardProps: cardProps) {
return ( return (
<> <article className="content-card group flex h-full w-full min-h-80 flex-col p-6">
<div className="bg-neutral-primary-soft shadow-lg shadow-blue-500/50 rounded-3xl block p-6 border-2 border-blue-500 rounded-base m-3 min-w-60 max-w-80"> <div className="content-card-body flex h-full flex-col">
<a href="#"> <a href="#" className="block shrink-0">
<img className="rounded-2xl" src={CardProps.src} alt="" /> <div className="content-card-image aspect-video w-full">
<img
className="h-full w-full object-cover"
src={CardProps.src}
alt=""
/>
</div>
</a> </a>
<a href="#"> <a href="#" className="mt-6 block shrink-0">
<h5 className="mt-6 mb-2 text-2xl font-semibold tracking-tight text-heading">{CardProps.heading}</h5> <h5 className="mb-2 text-2xl font-semibold tracking-tight text-heading">
{CardProps.heading}
</h5>
</a> </a>
<p className="mb-6 text-body">{CardProps.card_text}</p> <p className="mb-6 line-clamp-3 flex-1 text-body">{CardProps.card_text}</p>
<a href="#" className="inline-flex rounded-3xl items-center text-body bg-neutral-secondary-medium box-border border border-default-medium hover:bg-neutral-tertiary-medium hover:text-heading focus:ring-4 focus:ring-neutral-tertiary shadow-xs font-medium leading-5 rounded-base text-sm px-4 py-2.5 focus:outline-none"> <a href="#" className="content-card-cta mt-auto self-start">
{CardProps.button_text} {CardProps.button_text}
<svg className="w-3 h-3 ms-1.5 rtl:rotate-180 -me-0.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 12H5m14 0-4 4m4-4-4-4"/></svg> <svg
className="ms-1.5 -me-0.5 h-3 w-3 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 12H5m14 0-4 4m4-4-4-4"
/>
</svg>
</a> </a>
</div> </div>
</> </article>
) )
} }

View File

@@ -1,3 +0,0 @@
ignoredBuiltDependencies:
- sharp
- unrs-resolver