improved site header, added contact form, content card carousel, footer, and more photos
This commit is contained in:
@@ -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
167
components/contact-form.css
Normal 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;
|
||||
}
|
||||
}
|
||||
73
components/contact-form.tsx
Normal file
73
components/contact-form.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
62
components/content-card-carousel.css
Normal file
62
components/content-card-carousel.css
Normal 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);
|
||||
}
|
||||
126
components/content-card-carousel.tsx
Normal file
126
components/content-card-carousel.tsx
Normal 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"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={next}
|
||||
disabled={maxIndex === 0}
|
||||
className="carousel-control-btn"
|
||||
aria-label="Show next content cards"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
components/footer.tsx
Normal file
22
components/footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
.hero-main {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.hero-picture-stack {
|
||||
@@ -38,19 +39,9 @@
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
.hero-picture-stack {
|
||||
.herp-main {
|
||||
width: 100%;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.hero-picture {
|
||||
width: 75%;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.hero-right {
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
padding-top: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import ContentCard from "./ui/content-cards";
|
||||
import './hero-section.css'
|
||||
import heroPictureOne from "../assets/hero-picture-1.png"
|
||||
import heroPictureTwo from "../assets/hero-picture-2.png"
|
||||
import heroPictureThree from "../assets/hero-picture-3.png"
|
||||
import heroPictureOne from "../assets/mxj-files-planet-27066.gif"
|
||||
|
||||
export default function HeroSection() {
|
||||
return(
|
||||
<>
|
||||
<div className="hero-main flex flex-wrap">
|
||||
<div className="hero-picture-stack p-4">
|
||||
<img className="hero-picture" 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 className="flex flex-wrap-reverse justify-items-center ">
|
||||
<div className="p-4 w-full md:w-1/2 shrink-0">
|
||||
<img className="w-full h-auto rounded-2xl object-cover" src={heroPictureOne.src} alt="" />
|
||||
</div>
|
||||
<div className="hero-right">
|
||||
<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>
|
||||
<div className="w-full md:w-1/2">
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
|
||||
@@ -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">
|
||||
☰
|
||||
</span>
|
||||
<span className="burger-icon-close" aria-hidden="true">
|
||||
×
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,29 @@
|
||||
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 (
|
||||
<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">
|
||||
<img
|
||||
className="w-full h-auto rounded-2xl object-cover"
|
||||
src={picOne.src}
|
||||
src={content.src}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2">
|
||||
<h1 className="mb-4 text-5xl font-bold">Title of this section</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>
|
||||
<h1 className="mb-4 text-5xl font-bold">{content.title}</h1>
|
||||
<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-1 h-px bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="px-3">
|
||||
@@ -20,7 +31,7 @@ export default function PicWithText(){
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" />
|
||||
</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>
|
||||
)
|
||||
|
||||
463
components/site-header.css
Normal file
463
components/site-header.css
Normal 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
147
components/site-header.tsx
Normal 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">
|
||||
☰
|
||||
</span>
|
||||
<span className="burger-icon-close" aria-hidden="true">
|
||||
×
|
||||
</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>
|
||||
)
|
||||
}
|
||||
54
components/social-media-links.tsx
Normal file
54
components/social-media-links.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
137
components/ui/content-cards.css
Normal file
137
components/ui/content-cards.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import './content-cards.css'
|
||||
|
||||
interface cardProps {
|
||||
src?: string
|
||||
@@ -8,21 +9,45 @@ interface cardProps {
|
||||
}
|
||||
|
||||
export default function ContentCard(CardProps: cardProps) {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<a href="#">
|
||||
<img className="rounded-2xl" src={CardProps.src} alt="" />
|
||||
return (
|
||||
<article className="content-card group flex h-full w-full min-h-80 flex-col p-6">
|
||||
<div className="content-card-body flex h-full flex-col">
|
||||
<a href="#" className="block shrink-0">
|
||||
<div className="content-card-image aspect-video w-full">
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
src={CardProps.src}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#">
|
||||
<h5 className="mt-6 mb-2 text-2xl font-semibold tracking-tight text-heading">{CardProps.heading}</h5>
|
||||
<a href="#" className="mt-6 block shrink-0">
|
||||
<h5 className="mb-2 text-2xl font-semibold tracking-tight text-heading">
|
||||
{CardProps.heading}
|
||||
</h5>
|
||||
</a>
|
||||
<p className="mb-6 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">
|
||||
{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>
|
||||
<p className="mb-6 line-clamp-3 flex-1 text-body">{CardProps.card_text}</p>
|
||||
<a href="#" className="content-card-cta mt-auto self-start">
|
||||
{CardProps.button_text}
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user