add servies page with content and a typing words feature to contact page
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
|
||||
import PicWithText from "@/components/pic-with-text";
|
||||
import heda_bang from "../../assets/u_mey4kjj5ww-angry-2498.gif"
|
||||
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<h1>about us page</h1>
|
||||
<PicWithText src={heda_bang.src} text="Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" reverse={false} title="About us"/>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,23 @@
|
||||
import ContactForm from '@/components/contact-form'
|
||||
import ContactForm from '@/components/contact-form';
|
||||
import TypingWords from '@/components/typing-words';
|
||||
|
||||
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 className="flex w-full flex-col gap-10 lg:flex-row lg:items-start lg:gap-12 xl:gap-16">
|
||||
<div className="w-full lg:w-1/2 lg:max-w-xl">
|
||||
<h1 className="mb-4 text-4xl font-bold leading-tight text-heading sm:text-5xl md:text-6xl">
|
||||
Tell us about your
|
||||
</h1>
|
||||
<h1 className="mb-4 text-4xl font-bold leading-tight text-heading sm:text-5xl md:text-6xl">
|
||||
<TypingWords />
|
||||
</h1>
|
||||
<p className="text-lg text-body lg:text-xl">
|
||||
Share a few details and we'll get back to you as soon as we can.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
<div className="w-full lg:w-1/2 lg:flex lg:justify-end">
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ export default function RootLayout({
|
||||
>
|
||||
<body className="min-h-full flex flex-col dark:bg-black">
|
||||
<SiteHeader />
|
||||
<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 justify-between bg-white px-8 py-32 dark:bg-black sm:items-start">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -46,16 +46,13 @@ export default function Home() {
|
||||
}
|
||||
|
||||
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 justify-between bg-white px-8 py-32 dark:bg-black sm:items-start">
|
||||
|
||||
<>
|
||||
<HeroSection />
|
||||
<PicWithText {...picWithText} />
|
||||
|
||||
<div className="w-full m-auto">
|
||||
<ContentCardCarousel cards={contentCards} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<h1>plans page</h1>
|
||||
)
|
||||
}
|
||||
16
app/services/page.tsx
Normal file
16
app/services/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import ServicesFeature from '@/components/services-feature';
|
||||
|
||||
export default function ServicesPage() {
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-4 text-5xl font-bold text-heading md:text-6xl">
|
||||
Our <span className="text-transparent bg-clip-text bg-gradient-to-r from-sky-400 to-violet-800">Services</span>
|
||||
</h1>
|
||||
<p className="mb-12 max-w-2xl text-lg text-body lg:text-xl">
|
||||
Sculpted Arc helps companies build, launch, and grow online—from first design
|
||||
through long-term support.
|
||||
</p>
|
||||
<ServicesFeature />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
max-width: 36rem;
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
border-radius: 1.5rem;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
|
||||
152
components/services-feature.css
Normal file
152
components/services-feature.css
Normal file
@@ -0,0 +1,152 @@
|
||||
.services-feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.services-feature-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.services-feature-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.service-feature {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
height: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
.service-feature::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;
|
||||
}
|
||||
|
||||
.service-feature: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);
|
||||
}
|
||||
|
||||
.service-feature: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)
|
||||
);
|
||||
}
|
||||
|
||||
.service-feature-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.service-feature-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
color: #6d28d9;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(56, 189, 248, 0.15),
|
||||
rgba(109, 40, 217, 0.15)
|
||||
);
|
||||
}
|
||||
|
||||
.service-feature-title {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--foreground, #171717);
|
||||
}
|
||||
|
||||
.service-feature-description {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #52525b;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.service-feature {
|
||||
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);
|
||||
}
|
||||
|
||||
.service-feature: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);
|
||||
}
|
||||
|
||||
.service-feature-title {
|
||||
color: #ededed;
|
||||
}
|
||||
|
||||
.service-feature-description {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.service-feature,
|
||||
.service-feature::after {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.service-feature:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
97
components/services-feature.tsx
Normal file
97
components/services-feature.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import './services-feature.css';
|
||||
|
||||
export type Service = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const services: Service[] = [
|
||||
{
|
||||
title: 'Custom Web Design',
|
||||
description:
|
||||
'Tailored layouts and branding that reflect your business and engage your audience from the first visit.',
|
||||
},
|
||||
{
|
||||
title: 'Web Development',
|
||||
description:
|
||||
'Fast, reliable sites built with modern frameworks—scalable architecture that grows with your company.',
|
||||
},
|
||||
{
|
||||
title: 'E-commerce',
|
||||
description:
|
||||
'Online stores with smooth checkout, inventory management, and payment integrations that convert.',
|
||||
},
|
||||
{
|
||||
title: 'Performance & SEO',
|
||||
description:
|
||||
'Optimised load times, accessibility, and search visibility so customers can find and use your site.',
|
||||
},
|
||||
{
|
||||
title: 'Hosting & Maintenance',
|
||||
description:
|
||||
'Secure hosting, updates, backups, and monitoring so your site stays online and up to date.',
|
||||
},
|
||||
{
|
||||
title: 'APIs & Integrations',
|
||||
description:
|
||||
'Connect CRMs, booking systems, analytics, and third-party tools into one cohesive workflow.',
|
||||
},
|
||||
];
|
||||
|
||||
function ServiceFeatureCard({ title, description }: Service) {
|
||||
return (
|
||||
<article className="service-feature">
|
||||
<div className="service-feature-body">
|
||||
<div className="service-feature-icon" aria-hidden="true">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="service-feature-title">{title}</h3>
|
||||
<p className="service-feature-description">{description}</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
type ServicesFeatureProps = {
|
||||
heading?: string;
|
||||
subheading?: string;
|
||||
items?: Service[];
|
||||
};
|
||||
|
||||
export default function ServicesFeature({
|
||||
heading = 'What we offer',
|
||||
subheading = 'End-to-end web solutions for businesses of every size.',
|
||||
items = services,
|
||||
}: ServicesFeatureProps) {
|
||||
return (
|
||||
<section className="services-feature w-full">
|
||||
<header className="mb-10 max-w-2xl">
|
||||
<h2 className="mb-3 text-4xl font-bold tracking-tight text-heading md:text-5xl">
|
||||
{heading}
|
||||
</h2>
|
||||
<p className="text-lg text-body lg:text-xl">{subheading}</p>
|
||||
</header>
|
||||
<ul className="services-feature-grid">
|
||||
{items.map((service) => (
|
||||
<li key={service.title} className="list-none">
|
||||
<ServiceFeatureCard {...service} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import './site-header.css'
|
||||
const menuLinks = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'About', href: '/about-us' },
|
||||
{ label: 'Plans', href: '/plans' },
|
||||
{ label: 'Services', href: '/services' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
] as const
|
||||
|
||||
|
||||
25
components/typing-words.css
Normal file
25
components/typing-words.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.typing-words {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.typing-words-cursor {
|
||||
display: inline-block;
|
||||
margin-left: 0.05em;
|
||||
font-weight: 300;
|
||||
color: #38bdf8;
|
||||
animation: typing-cursor-blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes typing-cursor-blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.typing-words-cursor {
|
||||
animation: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
84
components/typing-words.tsx
Normal file
84
components/typing-words.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import './typing-words.css';
|
||||
|
||||
const WORDS = ['plan', 'idea', 'problem', 'goals'] as const;
|
||||
|
||||
const TYPING_MS = 90;
|
||||
const DELETING_MS = 55;
|
||||
const PAUSE_AFTER_TYPED_MS = 2000;
|
||||
const PAUSE_AFTER_DELETED_MS = 400;
|
||||
|
||||
type Phase = 'typing' | 'pausing' | 'deleting' | 'waiting';
|
||||
|
||||
export default function TypingWords() {
|
||||
const [wordIndex, setWordIndex] = useState(0);
|
||||
const [charIndex, setCharIndex] = useState(0);
|
||||
const [phase, setPhase] = useState<Phase>('typing');
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
setPrefersReducedMotion(media.matches);
|
||||
|
||||
const onChange = () => setPrefersReducedMotion(media.matches);
|
||||
media.addEventListener('change', onChange);
|
||||
return () => media.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
const currentWord = WORDS[wordIndex];
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (phase === 'typing') {
|
||||
if (charIndex < currentWord.length) {
|
||||
timeoutId = setTimeout(() => setCharIndex((i) => i + 1), TYPING_MS);
|
||||
} else {
|
||||
timeoutId = setTimeout(() => setPhase('pausing'), PAUSE_AFTER_TYPED_MS);
|
||||
}
|
||||
} else if (phase === 'pausing') {
|
||||
timeoutId = setTimeout(() => setPhase('deleting'), 0);
|
||||
} else if (phase === 'deleting') {
|
||||
if (charIndex > 0) {
|
||||
timeoutId = setTimeout(() => setCharIndex((i) => i - 1), DELETING_MS);
|
||||
} else {
|
||||
timeoutId = setTimeout(() => {
|
||||
setWordIndex((i) => (i + 1) % WORDS.length);
|
||||
setPhase('waiting');
|
||||
}, PAUSE_AFTER_DELETED_MS);
|
||||
}
|
||||
} else if (phase === 'waiting') {
|
||||
timeoutId = setTimeout(() => setPhase('typing'), 0);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [charIndex, phase, wordIndex, prefersReducedMotion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!prefersReducedMotion) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setWordIndex((i) => (i + 1) % WORDS.length);
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [prefersReducedMotion]);
|
||||
|
||||
const displayedWord = prefersReducedMotion
|
||||
? WORDS[wordIndex]
|
||||
: WORDS[wordIndex].slice(0, charIndex);
|
||||
|
||||
return (
|
||||
<span className="typing-words" aria-live="polite">
|
||||
<span className="typing-words-text text-transparent bg-clip-text bg-gradient-to-r from-sky-400 to-violet-800">
|
||||
{displayedWord}
|
||||
</span>
|
||||
<span className="typing-words-cursor" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user