mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add testimonials
This commit is contained in:
@@ -17,11 +17,12 @@
|
||||
"fumadocs-core": "15.1.2",
|
||||
"fumadocs-mdx": "11.5.7",
|
||||
"fumadocs-ui": "15.1.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"lucide-react": "^0.485.0",
|
||||
"motion": "^12.5.0",
|
||||
"next": "15.2.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tweet": "^3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
|
||||
@@ -53,15 +53,15 @@ const CodeContainer = () => {
|
||||
}, [typingComplete, currentStep]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto mt-8">
|
||||
<div className="w-full max-w-3xl mx-auto mt-4 sm:mt-8">
|
||||
<div className="rounded-xl overflow-hidden shadow-xl border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-black text-gray-800 dark:text-white">
|
||||
<div className="bg-gray-200 dark:bg-gray-800 px-4 py-2 flex items-center justify-between">
|
||||
<div className="bg-gray-200 dark:bg-gray-800 px-3 sm:px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<div className="font-mono text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="font-mono text-[10px] sm:text-xs text-gray-600 dark:text-gray-400 hidden xs:block">
|
||||
Quick Install Terminal
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ const CodeContainer = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center px-2 py-1 text-xs bg-gray-300/50 dark:bg-gray-800/50 rounded border border-gray-300 dark:border-gray-700 hover:bg-gray-300/80 dark:hover:bg-gray-700/50"
|
||||
className="flex items-center px-1.5 sm:px-2 py-1 text-[10px] sm:text-xs bg-gray-300/50 dark:bg-gray-800/50 rounded border border-gray-300 dark:border-gray-700 hover:bg-gray-300/80 dark:hover:bg-gray-700/50"
|
||||
>
|
||||
<Terminal className="w-3 h-3 mr-1 text-gray-600 dark:text-gray-400">
|
||||
<title>Package Manager</title>
|
||||
@@ -97,7 +97,7 @@ const CodeContainer = () => {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="absolute right-0 mt-1 w-36 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg z-50"
|
||||
className="absolute right-0 mt-1 w-32 sm:w-36 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg z-50"
|
||||
>
|
||||
<ul>
|
||||
{(Object.keys(commands) as Array<"npm" | "pnpm" | "bun">).map(
|
||||
@@ -105,7 +105,7 @@ const CodeContainer = () => {
|
||||
<li key={pm}>
|
||||
<button
|
||||
type="button"
|
||||
className={`block w-full text-left px-3 py-1.5 text-xs ${
|
||||
className={`block w-full text-left px-3 py-1.5 text-[10px] sm:text-xs ${
|
||||
selectedPM === pm
|
||||
? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-l-2 border-blue-500"
|
||||
: "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@@ -129,9 +129,9 @@ const CodeContainer = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 font-mono text-sm bg-gray-50 dark:bg-gray-900">
|
||||
<div className="p-3 sm:p-4 font-mono text-xs sm:text-sm bg-gray-50 dark:bg-gray-900 overflow-x-auto">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-grow">
|
||||
<div className="flex-grow overflow-x-auto">
|
||||
<span className="text-green-600 dark:text-green-400 mr-2">$</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{commands[selectedPM]}
|
||||
@@ -151,7 +151,7 @@ const CodeContainer = () => {
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(selectedPM)}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors flex-shrink-0 ml-2"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
@@ -167,8 +167,12 @@ const CodeContainer = () => {
|
||||
</div>
|
||||
|
||||
{typingComplete && (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
|
||||
<div className="mt-3 pl-4 text-amber-600 dark:text-amber-400">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="overflow-x-auto"
|
||||
>
|
||||
<div className="mt-3 pl-2 sm:pl-4 text-amber-600 dark:text-amber-400">
|
||||
{currentStep >= 1 && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
@@ -223,7 +227,7 @@ const CodeContainer = () => {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mt-3 pl-4"
|
||||
className="mt-3 pl-2 sm:pl-4"
|
||||
>
|
||||
{currentStep >= 3 && (
|
||||
<motion.p
|
||||
@@ -232,10 +236,10 @@ const CodeContainer = () => {
|
||||
transition={{ delay: 0.4 }}
|
||||
className="text-blue-600 dark:text-blue-400 flex items-center"
|
||||
>
|
||||
<CircleCheck className="w-4 h-4 mr-1">
|
||||
<CircleCheck className="w-4 h-4 mr-1 flex-shrink-0">
|
||||
<title>Completed</title>
|
||||
</CircleCheck>
|
||||
Creating project structure
|
||||
<span>Creating project structure</span>
|
||||
</motion.p>
|
||||
)}
|
||||
{currentStep >= 4 && (
|
||||
@@ -245,10 +249,10 @@ const CodeContainer = () => {
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-blue-600 dark:text-blue-400 flex items-center"
|
||||
>
|
||||
<CircleCheck className="w-4 h-4 mr-1">
|
||||
<CircleCheck className="w-4 h-4 mr-1 flex-shrink-0">
|
||||
<title>Completed</title>
|
||||
</CircleCheck>
|
||||
Installing dependencies
|
||||
<span>Installing dependencies</span>
|
||||
</motion.p>
|
||||
)}
|
||||
{currentStep >= 5 && (
|
||||
@@ -263,10 +267,10 @@ const CodeContainer = () => {
|
||||
transition={{ delay: 0.7 }}
|
||||
className="text-blue-600 dark:text-blue-400 flex items-center"
|
||||
>
|
||||
<CircleCheck className="w-4 h-4 mr-1">
|
||||
<CircleCheck className="w-4 h-4 mr-1 flex-shrink-0">
|
||||
<title>Completed</title>
|
||||
</CircleCheck>
|
||||
Setting up database schema
|
||||
<span>Setting up database schema</span>
|
||||
</motion.p>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, x: -5 }}
|
||||
@@ -274,19 +278,19 @@ const CodeContainer = () => {
|
||||
transition={{ delay: 0.8 }}
|
||||
className="text-blue-600 dark:text-blue-400 flex items-center"
|
||||
>
|
||||
<CircleCheck className="w-4 h-4 mr-1">
|
||||
<CircleCheck className="w-4 h-4 mr-1 flex-shrink-0">
|
||||
<title>Completed</title>
|
||||
</CircleCheck>
|
||||
Configuring authentication
|
||||
<span>Configuring authentication</span>
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.9 }}
|
||||
className="mt-4 flex items-center px-2 py-2 rounded bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 border border-blue-300 dark:border-blue-800/30"
|
||||
className="mt-4 flex flex-col xs:flex-row xs:items-center px-2 py-2 rounded bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 border border-blue-300 dark:border-blue-800/30 text-[10px] sm:text-xs"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 mr-2 text-blue-600 dark:text-blue-400"
|
||||
className="w-4 h-4 mb-1 xs:mb-0 xs:mr-2 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -298,18 +302,18 @@ const CodeContainer = () => {
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
<span>
|
||||
Project ready! Run{" "}
|
||||
<code className="px-1.5 py-0.5 bg-blue-200 dark:bg-blue-800/50 rounded">
|
||||
<div className="flex flex-wrap">
|
||||
<span className="mr-1">Project ready! Run</span>
|
||||
<code className="px-1 py-0.5 bg-blue-200 dark:bg-blue-800/50 rounded mb-1 xs:mb-0 mr-1">
|
||||
cd my-better-t-app
|
||||
</code>{" "}
|
||||
and{" "}
|
||||
<code className="px-1.5 py-0.5 bg-blue-200 dark:bg-blue-800/50 rounded">
|
||||
</code>
|
||||
<span className="mr-1">and</span>
|
||||
<code className="px-1 py-0.5 bg-blue-200 dark:bg-blue-800/50 rounded">
|
||||
{selectedPM === "npm" && "npm run dev"}
|
||||
{selectedPM === "pnpm" && "pnpm dev"}
|
||||
{selectedPM === "bun" && "bun dev"}
|
||||
</code>
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -329,11 +333,11 @@ const CodeContainer = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-200 dark:bg-gray-900 border-t border-gray-300 dark:border-gray-700 px-4 py-2">
|
||||
<div className="flex items-center justify-center text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<div className="bg-gray-200 dark:bg-gray-900 border-t border-gray-300 dark:border-gray-700 px-2 sm:px-4 py-2">
|
||||
<div className="flex items-center justify-center text-[10px] sm:text-xs text-gray-600 dark:text-gray-400 text-center">
|
||||
<span className="inline-flex flex-wrap items-center justify-center gap-1">
|
||||
<span>For custom options, use</span>
|
||||
<code className="px-1.5 py-0.5 bg-gray-300 dark:bg-gray-700 rounded">
|
||||
<code className="px-1 py-0.5 bg-gray-300 dark:bg-gray-700 rounded whitespace-nowrap">
|
||||
{selectedPM === "npm" && "npx"}
|
||||
{selectedPM === "pnpm" && "pnpm dlx"}
|
||||
{selectedPM === "bun" && "bunx"} create-better-t-stack
|
||||
|
||||
@@ -1,124 +1,184 @@
|
||||
import { useRef } from "react";
|
||||
"use client";
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: "$ user@dev.sh",
|
||||
role: "Senior",
|
||||
company: "TypeScript",
|
||||
avatar: ">_",
|
||||
content:
|
||||
"The type safety across the entire stack is exactly what I've been looking for. Incredible productivity boost.",
|
||||
},
|
||||
{
|
||||
name: "$ sarah@code.io",
|
||||
role: "Lead",
|
||||
company: "Engineer",
|
||||
avatar: "~/",
|
||||
content:
|
||||
"Better-T Stack simplified our deployment pipeline and improved our team's development experience tremendously.",
|
||||
},
|
||||
{
|
||||
name: "$ alex@terminal.dev",
|
||||
role: "Full-Stack",
|
||||
company: "Dev",
|
||||
avatar: "[]",
|
||||
content:
|
||||
"After switching to Better-T, our build times dropped by 60% and bug reports decreased significantly.",
|
||||
},
|
||||
import { motion } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tweet } from "react-tweet";
|
||||
|
||||
const TWEET_IDS = [
|
||||
"1904144343125860404",
|
||||
"1904215768272654825",
|
||||
"1904233896851521980",
|
||||
"1904228496144269699",
|
||||
"1904301540422070671",
|
||||
"1904338606409531710",
|
||||
"1904241046898556970",
|
||||
"1904318186750652606",
|
||||
"1904179661086556412",
|
||||
"1906149740095705265",
|
||||
"1906001923456790710",
|
||||
];
|
||||
|
||||
const Testimonials = () => {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
export default function Testimonials() {
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
const [tweetsPerPage, setTweetsPerPage] = useState(3);
|
||||
|
||||
const duplicatedTestimonials = [...testimonials];
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1280) {
|
||||
setTweetsPerPage(6);
|
||||
} else if (window.innerWidth >= 768) {
|
||||
setTweetsPerPage(4);
|
||||
} else if (window.innerWidth >= 640) {
|
||||
setTweetsPerPage(2);
|
||||
} else {
|
||||
setTweetsPerPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
// Get visible tweets
|
||||
const getVisibleTweets = () => {
|
||||
const visible = [];
|
||||
for (let i = 0; i < tweetsPerPage; i++) {
|
||||
const index = (startIndex + i) % TWEET_IDS.length;
|
||||
visible.push(index);
|
||||
}
|
||||
return visible;
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setStartIndex((prev) => (prev + tweetsPerPage) % TWEET_IDS.length);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setStartIndex((prev) => {
|
||||
const newIndex = prev - tweetsPerPage;
|
||||
return newIndex < 0 ? TWEET_IDS.length + newIndex : newIndex;
|
||||
});
|
||||
};
|
||||
|
||||
const visibleTweets = getVisibleTweets();
|
||||
const totalPages = Math.ceil(TWEET_IDS.length / tweetsPerPage);
|
||||
const currentPage = Math.floor(startIndex / tweetsPerPage) + 1;
|
||||
|
||||
return (
|
||||
<section className="py-20 relative overflow-hidden w-screen">
|
||||
<div className="absolute inset-0 opacity-90 z-0" />
|
||||
|
||||
<div className="w-full mx-auto relative z-10">
|
||||
<div className="text-center mb-16 max-w-6xl mx-auto">
|
||||
<h2 className="text-4xl font-bold text-gray-900 dark:text-white">
|
||||
<span className="text-gray-600 dark:text-gray-400">$</span> cat
|
||||
testimonials.log
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-4 text-lg font-mono">
|
||||
<span className="text-gray-500">Output:</span> Users reporting
|
||||
success with Better-T Stack
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full overflow-hidden relative">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[100px] z-10 bg-gradient-to-r from-white to-transparent dark:from-black pointer-events-none" />
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex animate-scroll px-[5%]"
|
||||
style={{
|
||||
animation: "scroll 30s linear infinite",
|
||||
willChange: "transform",
|
||||
}}
|
||||
>
|
||||
{duplicatedTestimonials.map((testimonial) => (
|
||||
<div
|
||||
key={testimonial.name}
|
||||
className="group rounded-md border border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-black/10 p-6 hover:border-blue-500/30 transition-colors duration-200 w-[25%] md:w-[30%] mx-4 flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<div className="shrink-0">
|
||||
<div className="w-10 h-10 rounded-sm bg-gradient-to-r from-gray-100 to-gray-200 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center text-gray-700 dark:text-gray-300 font-mono">
|
||||
{testimonial.avatar}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-gray-900 dark:text-white font-mono">
|
||||
{testimonial.name}
|
||||
</h3>
|
||||
<p className="text-sm font-mono">
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{testimonial.role}
|
||||
</span>
|
||||
<span className="text-gray-500 mx-1">@</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{testimonial.company}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 dark:text-gray-300 leading-relaxed font-mono border-l-2 border-blue-700 pl-4">
|
||||
{testimonial.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute right-0 top-0 bottom-0 w-[100px] z-10 bg-gradient-to-l from-white to-transparent dark:from-black pointer-events-none" />
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12 max-w-6xl mx-auto">
|
||||
<div className="inline-block py-2 px-4 bg-gray-50 dark:bg-black border border-gray-200 dark:border-gray-800 rounded-md">
|
||||
<span className="text-blue-500 font-bold mr-2">$</span>
|
||||
<span className="text-gray-900 dark:text-white font-mono">
|
||||
Join the growing community of developers
|
||||
<section className="w-full max-w-7xl mx-auto space-y-16 mt-20 relative z-10 px-4 sm:px-6">
|
||||
<div className="text-center space-y-8 relative">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative"
|
||||
>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold">
|
||||
<span className="text-blue-500 dark:text-blue-400 font-mono mr-1">
|
||||
{">"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-indigo-600 dark:from-blue-400 dark:to-indigo-500">
|
||||
Developer Feedback
|
||||
</span>
|
||||
</h2>
|
||||
<div className="absolute -inset-x-1/4 -inset-y-1/2 bg-gradient-to-r from-blue-300/0 via-blue-300/10 to-blue-300/0 dark:from-blue-800/0 dark:via-blue-800/10 dark:to-blue-800/0 blur-3xl -z-10" />
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="text-lg sm:text-xl text-gray-700 dark:text-gray-300 leading-relaxed font-mono max-w-3xl mx-auto"
|
||||
>
|
||||
what devs are saying about Better-T-Stack
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-29% * ${testimonials.length}));
|
||||
}
|
||||
}
|
||||
.animate-scroll:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
`}</style>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
className="relative mt-8"
|
||||
>
|
||||
<div className="rounded-xl overflow-hidden shadow-xl border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-900">
|
||||
<div className="bg-gray-200 dark:bg-gray-800 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<div className="font-mono text-xs text-gray-600 dark:text-gray-400">
|
||||
Developer Feedback Terminal
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handlePrev}
|
||||
className="h-6 w-6 flex items-center justify-center rounded bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-400 dark:hover:bg-gray-600 transition-colors"
|
||||
title="Previous testimonials"
|
||||
>
|
||||
<ChevronLeft className="h-3 w-3" />
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleNext}
|
||||
className="h-6 w-6 flex items-center justify-center rounded bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-400 dark:hover:bg-gray-600 transition-colors"
|
||||
title="Next testimonials"
|
||||
>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{visibleTweets.map((tweetIndex) => (
|
||||
<Tweet key={tweetIndex} id={TWEET_IDS[tweetIndex]} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-200 dark:bg-gray-800 border-t border-gray-300 dark:border-gray-700 p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden sm:flex items-center gap-1">
|
||||
{Array.from({ length: totalPages }).map((_, i) => {
|
||||
const isActive = i === currentPage - 1;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
key={i}
|
||||
onClick={() => setStartIndex(i * tweetsPerPage)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-colors ${
|
||||
isActive
|
||||
? "bg-blue-500"
|
||||
: "bg-gray-400 dark:bg-gray-600 hover:bg-gray-500"
|
||||
}`}
|
||||
aria-label={`Go to page ${i + 1}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/5 via-transparent to-indigo-500/5 blur-3xl -z-10" />
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Testimonials;
|
||||
}
|
||||
|
||||
@@ -5,38 +5,38 @@ import BackgroundGradients from "./_components/BackgroundGradients";
|
||||
import CodeContainer from "./_components/CodeContainer";
|
||||
import CustomizableSection from "./_components/CustomizableSection";
|
||||
import NpmPackage from "./_components/NpmPackage";
|
||||
// import Testimonials from "./_components/Testimonials";
|
||||
import Testimonials from "./_components/Testimonials";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-start px-4 sm:px-8 pt-28 pb-16">
|
||||
<main className="flex flex-col items-center justify-start px-2 sm:px-4 md:px-8 pt-16 sm:pt-20 md:pt-28 pb-10 sm:pb-16">
|
||||
<BackgroundGradients />
|
||||
<div className="max-w-5xl mx-auto text-center mb-16 relative z-10">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<h1 className="text-5xl sm:text-7xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
<div className="max-w-5xl mx-auto text-center mb-10 sm:mb-16 relative z-10">
|
||||
<div className="px-1 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 sm:space-y-4 text-center">
|
||||
<h1 className="text-4xl xs:text-5xl sm:text-6xl md:text-7xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
<span className="block bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-indigo-600 pb-1">
|
||||
Better-T Stack
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div className="mb-2">
|
||||
<div className="mb-1 sm:mb-2">
|
||||
<NpmPackage />
|
||||
</div>
|
||||
|
||||
<p className="text-xl font-medium text-gray-600 dark:text-gray-300 max-w-2xl">
|
||||
<p className="text-lg sm:text-xl font-medium text-gray-600 dark:text-gray-300 max-w-2xl px-1">
|
||||
A modern CLI tool for scaffolding end-to-end type-safe TypeScript
|
||||
projects with best practices and customizable configurations
|
||||
</p>
|
||||
|
||||
<div className="w-full max-w-3xl mx-auto mt-2">
|
||||
<div className="w-full max-w-3xl mx-auto mt-1 sm:mt-2">
|
||||
<CodeContainer />
|
||||
</div>
|
||||
|
||||
<ShinyText
|
||||
text="Type-safe. Modern. Minimal. Fast."
|
||||
speed={3}
|
||||
className="text-sm sm:text-base text-gray-600 dark:text-gray-400"
|
||||
className="text-xs xs:text-sm sm:text-base text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,8 +45,10 @@ export default function HomePage() {
|
||||
<div className="absolute inset-0 bg-gradient-to-r dark:from-purple-500/20 dark:to-indigo-500/20 from-blue-300/20 to-indigo-300/20 dark:blur-3xl blur-2xl transform -skew-y-12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomizableSection />
|
||||
<div className="w-full pt-12 relative">
|
||||
|
||||
<div className="w-full pt-8 sm:pt-12 relative">
|
||||
<div className="max-w-5xl mx-auto relative">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="hidden sm:flex items-center w-1/3">
|
||||
@@ -54,11 +56,11 @@ export default function HomePage() {
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500/50" />
|
||||
</div>
|
||||
|
||||
<div className="px-6">
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-md">
|
||||
<div className="px-4 sm:px-6">
|
||||
<div className="h-7 w-7 sm:h-8 sm:w-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-md">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 text-white"
|
||||
className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-white"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -81,7 +83,8 @@ export default function HomePage() {
|
||||
<div className="sm:hidden h-px w-full mt-6 bg-gradient-to-r from-blue-500/20 via-indigo-500/40 to-blue-500/20" />
|
||||
</div>
|
||||
</div>
|
||||
{/* <Testimonials /> */}
|
||||
|
||||
<Testimonials />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%' height='100%' filter='url(%23noise)' opacity='0.4'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.react-tweet-theme {
|
||||
--tweet-container-margin: 0 !important;
|
||||
}
|
||||
|
||||
.shiny-text {
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
|
||||
13
bun.lock
13
bun.lock
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "create-better-t-stack",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.3",
|
||||
"bin": {
|
||||
"create-better-t-stack": "dist/index.js",
|
||||
},
|
||||
@@ -44,11 +44,12 @@
|
||||
"fumadocs-core": "15.1.2",
|
||||
"fumadocs-mdx": "11.5.7",
|
||||
"fumadocs-ui": "15.1.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"lucide-react": "^0.485.0",
|
||||
"motion": "^12.5.0",
|
||||
"next": "15.2.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tweet": "^3.2.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
@@ -1113,7 +1114,7 @@
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.483.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WldsY17Qb/T3VZdMnVQ9C3DDIP7h1ViDTHVdVGnLZcvHNg30zH/MTQ04RTORjexoGmpsXroiQXZ4QyR0kBy0FA=="],
|
||||
"lucide-react": ["lucide-react@0.485.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NvyQJ0LKyyCxL23nPKESlr/jmz8r7fJO1bkuptSNYSy0s8VVj4ojhX0YAgmE1e0ewfxUZjIlZpvH+otfTnla8Q=="],
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
@@ -1365,6 +1366,8 @@
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"react-tweet": ["react-tweet@3.2.2", "", { "dependencies": { "@swc/helpers": "^0.5.3", "clsx": "^2.0.0", "swr": "^2.2.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-hIkxAVPpN2RqWoDEbo3TTnN/pDcp9/Jb6pTgiA4EbXa9S+m2vHIvvZKHR+eS0PDIsYqe+zTmANRa5k6+/iwGog=="],
|
||||
|
||||
"read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.1", "", {}, "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw=="],
|
||||
@@ -1525,6 +1528,8 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.0.15", "", {}, "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg=="],
|
||||
@@ -1725,6 +1730,8 @@
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"fumadocs-ui/lucide-react": ["lucide-react@0.483.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WldsY17Qb/T3VZdMnVQ9C3DDIP7h1ViDTHVdVGnLZcvHNg30zH/MTQ04RTORjexoGmpsXroiQXZ4QyR0kBy0FA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||
|
||||
Reference in New Issue
Block a user