Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_ADMIN_DOMAIN=http://localhost:3000
VITE_ADMIN_API_TOKEN=cfp_YOUR_TOKEN
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dist-ssr

.env
.env.*
!.env.example

# TanStack Start / build output
.output
Expand Down
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@radix-ui/react-slot": "^1.3.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest",
"@tanstack/react-query": "^5.101.4",
"@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest",
"@tanstack/react-start": "latest",
Expand Down
43 changes: 18 additions & 25 deletions src/components/sections/Testimonials.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import { Quote } from 'lucide-react';

import type { ClientFeedback } from '#/lib/client-feedback.functions.ts';

import { Reveal } from '../ui/Reveal';
import { Section } from '../ui/Section';

const testimonials = [
{
quote:
"An exceptional engineer who doesn't just write code, but solves complex business problems. Delivered our entire cloud infrastructure ahead of schedule.",
author: 'Sarah J.',
role: 'CTO, TechStartup',
},
{
quote:
'Transformed our legacy monolith into a performant, scalable Next.js and Node.js architecture. The performance gains were immediate.',
author: 'Michael T.',
role: 'Engineering Manager',
},
{
quote:
'Clean code, excellent communication, and a deep understanding of modern SaaS architecture. Highly recommended for any serious technical project.',
author: 'Elena R.',
role: 'Founder',
},
];
type TestimonialsProps = {
testimonials: ClientFeedback[];
};

function formatAuthorRole(testimonial: ClientFeedback) {
return [testimonial.authorRole, testimonial.authorCompany]
.filter(Boolean)
.join(', ');
}

export function Testimonials() {
export function Testimonials({ testimonials }: TestimonialsProps) {
return (
<Section id="testimonials" className="bg-background relative">
<Reveal>
Expand All @@ -40,19 +31,21 @@ export function Testimonials() {

<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{testimonials.map((t, i) => (
<Reveal key={t.author} delay={i * 0.1}>
<Reveal key={t.id} delay={i * 0.1}>
<div className="glass-panel relative flex h-full flex-col rounded-3xl p-8">
<Quote className="text-accent-primary/20 absolute top-6 right-6 h-10 w-10" />
<p className="text-text-secondary relative z-10 mb-8 flex-1 leading-relaxed">
"{t.quote}"
</p>
<div className="mt-auto flex items-center gap-4">
<div className="bg-surface border-border text-text-muted flex h-12 w-12 items-center justify-center rounded-full border font-bold">
{t.author.charAt(0)}
{t.authorName.charAt(0)}
</div>
<div>
<div className="font-bold text-white">{t.author}</div>
<div className="text-text-muted text-sm">{t.role}</div>
<div className="font-bold text-white">{t.authorName}</div>
<div className="text-text-muted text-sm">
{formatAuthorRole(t)}
</div>
</div>
</div>
</div>
Expand Down
50 changes: 50 additions & 0 deletions src/lib/client-feedback.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createServerFn } from '@tanstack/react-start';

const CLIENT_FEEDBACK_PATH = '/api/client-feedback';

export type ClientFeedback = {
id: string;
quote: string;
authorName: string;
authorRole: string;
authorCompany: string;
createdAt: string;
updatedAt: string;
};

type ClientFeedbackResponse = {
data: ClientFeedback[];
page: number;
limit: number;
total: number;
};

export const getClientFeedback = createServerFn({ method: 'GET' }).handler(
async (): Promise<ClientFeedback[]> => {
const baseUrl = import.meta.env.VITE_ADMIN_DOMAIN;
const token = import.meta.env.VITE_ADMIN_API_TOKEN;

if (!baseUrl || !token) {
return [];
}

const url = new URL(CLIENT_FEEDBACK_PATH, baseUrl);
url.searchParams.set('page', '1');
url.searchParams.set('limit', '20');

const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
throw new Error(
`Failed to fetch client feedback: ${response.status} ${response.statusText}`,
);
}

const payload = (await response.json()) as ClientFeedbackResponse;
return payload.data;
},
);
11 changes: 11 additions & 0 deletions src/lib/query-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { QueryClient } from '@tanstack/react-query';

export function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
},
},
});
}
5 changes: 5 additions & 0 deletions src/lib/router-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { QueryClient } from '@tanstack/react-query';

export interface RouterContext {
queryClient: QueryClient;
}
11 changes: 11 additions & 0 deletions src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router';

import { createQueryClient } from '#/lib/query-client.ts';
import type { RouterContext } from '#/lib/router-context.ts';

import { routeTree } from './routeTree.gen';

export type { RouterContext };

export function getRouter() {
const queryClient = createQueryClient();

const router = createTanStackRouter({
routeTree,
context: {
queryClient,
},
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
Expand Down
40 changes: 25 additions & 15 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router';
import { QueryClientProvider } from '@tanstack/react-query';
import {
HeadContent,
Scripts,
createRootRouteWithContext,
} from '@tanstack/react-router';
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
import { TanStackDevtools } from '@tanstack/react-devtools';

import appCss from '../styles.css?url';
import Navigation from '#/components/layout/Navigation.tsx';
import { siteConfig } from '#/config/site.ts';
import { buildStructuredDataScripts } from '#/lib/seo.ts';
import type { RouterContext } from '#/lib/router-context.ts';

export const Route = createRootRoute({
export const Route = createRootRouteWithContext<RouterContext>()({
head: () => ({
meta: [
{ charSet: 'utf-8' },
Expand Down Expand Up @@ -45,25 +51,29 @@ export const Route = createRootRoute({
});

function RootDocument({ children }: { children: React.ReactNode }) {
const { queryClient } = Route.useRouteContext();

return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<Navigation />
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
<QueryClientProvider client={queryClient}>
<Navigation />
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
</QueryClientProvider>
<Scripts />
</body>
</html>
Expand Down
10 changes: 9 additions & 1 deletion src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { seo } from '#/lib/seo.ts';
import { getClientFeedback } from '#/lib/client-feedback.functions.ts';
import { HeroSection } from '#/components/sections/HeroSection.tsx';
import { TrustStrip } from '#/components/sections/TrustStrip.tsx';
import { Metrics } from '#/components/sections/Metrics.tsx';
Expand All @@ -14,6 +15,11 @@ import { Contact } from '#/components/sections/Contact.tsx';
import { Footer } from '#/components/sections/Footer.tsx';

export const Route = createFileRoute('/')({
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData({
queryKey: ['client-feedback'],
queryFn: () => getClientFeedback(),
}),
head: () => {
const { meta, links } = seo({ url: '/' });

Expand All @@ -23,6 +29,8 @@ export const Route = createFileRoute('/')({
});

function Home() {
const testimonials = Route.useLoaderData();

return (
<main className="bg-background min-h-screen">
<HeroSection />
Expand All @@ -33,7 +41,7 @@ function Home() {
<TechStack />
<Process />
<About />
<Testimonials />
<Testimonials testimonials={testimonials} />
<FAQ />
<Contact />
<Footer />
Expand Down
Loading