chore: format

This commit is contained in:
2025-10-10 16:43:21 +02:00
parent f0aabd63b6
commit 75c29e0ba4
551 changed files with 433948 additions and 94145 deletions

View File

@@ -3,274 +3,274 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.5% 48%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.5% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.animation-delay-500 {
animation-delay: 500ms;
}
.animation-delay-500 {
animation-delay: 500ms;
}
.animation-delay-700 {
animation-delay: 700ms;
}
.animation-delay-700 {
animation-delay: 700ms;
}
/* Space-themed animations */
.perspective {
perspective: 1000px;
transform-style: preserve-3d;
}
/* Space-themed animations */
.perspective {
perspective: 1000px;
transform-style: preserve-3d;
}
.envelope-flap {
transform-origin: top;
transform: rotateX(20deg);
transition: transform 0.3s ease;
}
.envelope-flap {
transform-origin: top;
transform: rotateX(20deg);
transition: transform 0.3s ease;
}
.letter {
transform-origin: center;
transition: transform 0.3s ease;
}
.letter {
transform-origin: center;
transition: transform 0.3s ease;
}
@keyframes float-space {
0% {
transform: translateY(0px) rotate(6deg) translateZ(0px);
}
50% {
transform: translateY(-10px) rotate(6deg) translateZ(10px);
}
100% {
transform: translateY(0px) rotate(6deg) translateZ(0px);
}
}
@keyframes float-space {
0% {
transform: translateY(0px) rotate(6deg) translateZ(0px);
}
50% {
transform: translateY(-10px) rotate(6deg) translateZ(10px);
}
100% {
transform: translateY(0px) rotate(6deg) translateZ(0px);
}
}
.animate-float-space {
animation: float-space 6s ease-in-out infinite;
}
.animate-float-space {
animation: float-space 6s ease-in-out infinite;
}
@keyframes orbit {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes orbit {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.animate-orbit {
animation: orbit 20s linear infinite;
}
.animate-orbit {
animation: orbit 20s linear infinite;
}
.animate-orbit-reverse {
animation: orbit 15s linear infinite reverse;
}
.animate-orbit-reverse {
animation: orbit 15s linear infinite reverse;
}
.animate-orbit-slow {
animation: orbit 30s linear infinite;
}
.animate-orbit-slow {
animation: orbit 30s linear infinite;
}
@keyframes twinkle {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.8);
}
}
@keyframes twinkle {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.8);
}
}
.animate-twinkle {
animation: twinkle 2s ease-in-out infinite;
}
.animate-twinkle {
animation: twinkle 2s ease-in-out infinite;
}
/* Stars */
.star {
@apply absolute rounded-full bg-white;
width: 2px;
height: 2px;
opacity: 0.7;
}
/* Stars */
.star {
@apply absolute rounded-full bg-white;
width: 2px;
height: 2px;
opacity: 0.7;
}
.star-1 {
top: 10%;
left: 20%;
animation: twinkle 3s infinite 0.1s;
}
.star-1 {
top: 10%;
left: 20%;
animation: twinkle 3s infinite 0.1s;
}
.star-2 {
top: 30%;
left: 70%;
animation: twinkle 2s infinite 0.3s;
}
.star-2 {
top: 30%;
left: 70%;
animation: twinkle 2s infinite 0.3s;
}
.star-3 {
top: 60%;
left: 30%;
animation: twinkle 4s infinite 0.5s;
}
.star-3 {
top: 60%;
left: 30%;
animation: twinkle 4s infinite 0.5s;
}
.star-4 {
top: 80%;
left: 80%;
animation: twinkle 3s infinite 0.7s;
}
.star-4 {
top: 80%;
left: 80%;
animation: twinkle 3s infinite 0.7s;
}
.star-5 {
top: 15%;
left: 50%;
animation: twinkle 5s infinite 0.2s;
}
.star-5 {
top: 15%;
left: 50%;
animation: twinkle 5s infinite 0.2s;
}
.star-6 {
top: 45%;
left: 15%;
animation: twinkle 2.5s infinite 0.4s;
}
.star-6 {
top: 45%;
left: 15%;
animation: twinkle 2.5s infinite 0.4s;
}
.star-7 {
top: 75%;
left: 45%;
animation: twinkle 3.5s infinite 0.6s;
}
.star-7 {
top: 75%;
left: 45%;
animation: twinkle 3.5s infinite 0.6s;
}
.star-8 {
top: 25%;
left: 85%;
animation: twinkle 4.5s infinite 0.8s;
}
.star-8 {
top: 25%;
left: 85%;
animation: twinkle 4.5s infinite 0.8s;
}
.star-9 {
top: 90%;
left: 10%;
animation: twinkle 3s infinite 0.9s;
}
.star-9 {
top: 90%;
left: 10%;
animation: twinkle 3s infinite 0.9s;
}
.star-10 {
top: 5%;
left: 35%;
animation: twinkle 2s infinite 1s;
}
.star-10 {
top: 5%;
left: 35%;
animation: twinkle 2s infinite 1s;
}
.star-11 {
top: 50%;
left: 95%;
animation: twinkle 4s infinite 1.1s;
}
.star-11 {
top: 50%;
left: 95%;
animation: twinkle 4s infinite 1.1s;
}
.star-12 {
top: 35%;
left: 5%;
animation: twinkle 3s infinite 1.2s;
}
.star-12 {
top: 35%;
left: 5%;
animation: twinkle 3s infinite 1.2s;
}
/* Particles */
.particle {
@apply absolute rounded-full;
width: 3px;
height: 3px;
opacity: 0.7;
}
/* Particles */
.particle {
@apply absolute rounded-full;
width: 3px;
height: 3px;
opacity: 0.7;
}
.particle-1 {
@apply bg-[#4ECDC4];
top: 30%;
left: 40%;
animation: particle-float 8s infinite linear;
}
.particle-1 {
@apply bg-[#4ECDC4];
top: 30%;
left: 40%;
animation: particle-float 8s infinite linear;
}
.particle-2 {
@apply bg-yellow-400;
top: 60%;
left: 60%;
animation: particle-float 12s infinite 2s linear;
}
.particle-2 {
@apply bg-yellow-400;
top: 60%;
left: 60%;
animation: particle-float 12s infinite 2s linear;
}
.particle-3 {
@apply bg-white;
top: 40%;
left: 70%;
animation: particle-float 10s infinite 1s linear;
}
.particle-3 {
@apply bg-white;
top: 40%;
left: 70%;
animation: particle-float 10s infinite 1s linear;
}
.particle-4 {
@apply bg-[#4ECDC4];
top: 70%;
left: 30%;
animation: particle-float 9s infinite 3s linear;
}
.particle-4 {
@apply bg-[#4ECDC4];
top: 70%;
left: 30%;
animation: particle-float 9s infinite 3s linear;
}
.particle-5 {
@apply bg-yellow-400;
top: 20%;
left: 50%;
animation: particle-float 11s infinite 4s linear;
}
.particle-5 {
@apply bg-yellow-400;
top: 20%;
left: 50%;
animation: particle-float 11s infinite 4s linear;
}
@keyframes particle-float {
0% {
transform: translate(0, 0);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translate(calc(random(100) * 1px), calc(random(100) * 1px));
opacity: 0;
}
}
@keyframes particle-float {
0% {
transform: translate(0, 0);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translate(calc(random(100) * 1px), calc(random(100) * 1px));
opacity: 0;
}
}
}

View File

@@ -1,86 +1,86 @@
import type { Metadata, Viewport } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css"
import { Analytics } from "@/components/analytics"
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Analytics } from "@/components/analytics";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
})
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
})
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "LetterSpace | Self-hosted Newsletter Platform",
description:
"Open source newsletter platform for managing your subscribers and sending beautiful emails, all self-hosted for complete control and privacy.",
keywords: [
"newsletter",
"email marketing",
"self-hosted",
"open source",
"subscriber management",
],
authors: [{ name: "LetterSpace Team" }],
creator: "LetterSpace",
publisher: "LetterSpace",
metadataBase: new URL("https://letterspace.app"),
alternates: {
canonical: "/",
},
robots: {
index: true,
follow: true,
},
openGraph: {
type: "website",
title: "LetterSpace | Self-hosted Newsletter Platform",
description:
"Open source newsletter platform for managing your subscribers and sending beautiful emails, all self-hosted for complete control and privacy.",
siteName: "LetterSpace",
images: [
{
url: "/cover.png",
width: 1200,
height: 630,
alt: "LetterSpace - Self-hosted Newsletter Platform",
},
],
},
twitter: {
card: "summary_large_image",
title: "LetterSpace | Self-hosted Newsletter Platform",
description:
"Open source newsletter platform for managing your subscribers and sending beautiful emails, all self-hosted for complete control and privacy.",
images: ["/cover.png"],
creator: "@letterspace",
},
applicationName: "LetterSpace",
category: "Email Marketing",
}
title: "LetterSpace | Self-hosted Newsletter Platform",
description:
"Open source newsletter platform for managing your subscribers and sending beautiful emails, all self-hosted for complete control and privacy.",
keywords: [
"newsletter",
"email marketing",
"self-hosted",
"open source",
"subscriber management",
],
authors: [{ name: "LetterSpace Team" }],
creator: "LetterSpace",
publisher: "LetterSpace",
metadataBase: new URL("https://letterspace.app"),
alternates: {
canonical: "/",
},
robots: {
index: true,
follow: true,
},
openGraph: {
type: "website",
title: "LetterSpace | Self-hosted Newsletter Platform",
description:
"Open source newsletter platform for managing your subscribers and sending beautiful emails, all self-hosted for complete control and privacy.",
siteName: "LetterSpace",
images: [
{
url: "/cover.png",
width: 1200,
height: 630,
alt: "LetterSpace - Self-hosted Newsletter Platform",
},
],
},
twitter: {
card: "summary_large_image",
title: "LetterSpace | Self-hosted Newsletter Platform",
description:
"Open source newsletter platform for managing your subscribers and sending beautiful emails, all self-hosted for complete control and privacy.",
images: ["/cover.png"],
creator: "@letterspace",
},
applicationName: "LetterSpace",
category: "Email Marketing",
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
}
width: "device-width",
initialScale: 1,
maximumScale: 1,
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Analytics />
</body>
</html>
)
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Analytics />
</body>
</html>
);
}

View File

@@ -1,15 +1,15 @@
import { Header } from "@/components/header"
import { Hero } from "@/components/hero"
import { Features, featuresData } from "@/components/features"
import { Footer } from "@/components/footer"
import { Header } from "@/components/header";
import { Hero } from "@/components/hero";
import { Features, featuresData } from "@/components/features";
import { Footer } from "@/components/footer";
export default function Home() {
return (
<div className="min-h-screen bg-[#121212] text-white">
<Header />
<Hero />
<Features features={featuresData} />
<Footer />
</div>
)
return (
<div className="min-h-screen bg-[#121212] text-white">
<Header />
<Hero />
<Features features={featuresData} />
<Footer />
</div>
);
}

View File

@@ -1,13 +1,13 @@
import Script from "next/script"
import Script from "next/script";
export function Analytics() {
return (
<Script
id="plausible-script"
strategy="afterInteractive"
defer
data-domain="letterspace.app"
src="https://analytics.letterspace.app/js/script.js"
/>
)
return (
<Script
id="plausible-script"
strategy="afterInteractive"
defer
data-domain="letterspace.app"
src="https://analytics.letterspace.app/js/script.js"
/>
);
}

View File

@@ -1,115 +1,115 @@
"use client"
"use client";
import Link from "next/link"
import type { LucideIcon } from "lucide-react"
import Link from "next/link";
import type { LucideIcon } from "lucide-react";
import {
Zap,
Wifi,
Database,
MessageSquare,
FileCode,
Settings,
BugPlay,
ChevronDown,
} from "lucide-react"
import constants from "@/constants"
Zap,
Wifi,
Database,
MessageSquare,
FileCode,
Settings,
BugPlay,
ChevronDown,
} from "lucide-react";
import constants from "@/constants";
interface Feature {
icon: React.ReactElement<LucideIcon>
title: string
description: string
link?: {
text: string
url: string
}
icon: React.ReactElement<LucideIcon>;
title: string;
description: string;
link?: {
text: string;
url: string;
};
}
export const featuresData: Feature[] = [
{
icon: <Zap className="h-6 w-6 text-yellow-400" />,
title: "Zero-Config",
description: "Sensible built-in default configs for common use cases",
},
// {
// icon: <Code className="h-6 w-6 text-[#4ECDC4]" />,
// title: "Extensible",
// description:
// "Expose the full ability to customize the behavior of the platform",
// },
{
icon: <Wifi className="h-6 w-6 text-gray-300" />,
title: "SMTP Support",
description: "Connect any SMTP server to send emails to your subscribers",
},
{
icon: <Database className="h-6 w-6 text-[#4ECDC4]" />,
title: "Unlimited Lists",
description: "Create and manage as many newsletter lists as you need",
},
{
icon: <MessageSquare className="h-6 w-6 text-gray-300" />,
title: "Campaign Management",
description: "Built-in support for creating and scheduling email campaigns",
},
// {
// icon: <RefreshCw className="h-6 w-6 text-[#4ECDC4]" />,
// title: "Auto-scaling",
// description: "Automatically handle thousands of subscribers with ease",
// },
{
icon: <FileCode className="h-6 w-6 text-yellow-400" />,
title: "REST API",
description: "Comprehensive API for integrating with your applications",
link: {
text: "API Documentation",
url: constants.env.DOCS_URL + "/api",
},
},
{
icon: <Settings className="h-6 w-6 text-gray-300" />,
title: "Advanced Analytics",
description: "Track opens, clicks, and engagement for all your campaigns",
},
{
icon: <BugPlay className="h-6 w-6 text-[#4ECDC4]" />,
title: "Built by Developers",
description:
"For developers, by developers. Everything you need to build and test with ease.",
link: {
text: "Development Guide",
url: constants.env.DOCS_URL,
},
},
]
{
icon: <Zap className="h-6 w-6 text-yellow-400" />,
title: "Zero-Config",
description: "Sensible built-in default configs for common use cases",
},
// {
// icon: <Code className="h-6 w-6 text-[#4ECDC4]" />,
// title: "Extensible",
// description:
// "Expose the full ability to customize the behavior of the platform",
// },
{
icon: <Wifi className="h-6 w-6 text-gray-300" />,
title: "SMTP Support",
description: "Connect any SMTP server to send emails to your subscribers",
},
{
icon: <Database className="h-6 w-6 text-[#4ECDC4]" />,
title: "Unlimited Lists",
description: "Create and manage as many newsletter lists as you need",
},
{
icon: <MessageSquare className="h-6 w-6 text-gray-300" />,
title: "Campaign Management",
description: "Built-in support for creating and scheduling email campaigns",
},
// {
// icon: <RefreshCw className="h-6 w-6 text-[#4ECDC4]" />,
// title: "Auto-scaling",
// description: "Automatically handle thousands of subscribers with ease",
// },
{
icon: <FileCode className="h-6 w-6 text-yellow-400" />,
title: "REST API",
description: "Comprehensive API for integrating with your applications",
link: {
text: "API Documentation",
url: constants.env.DOCS_URL + "/api",
},
},
{
icon: <Settings className="h-6 w-6 text-gray-300" />,
title: "Advanced Analytics",
description: "Track opens, clicks, and engagement for all your campaigns",
},
{
icon: <BugPlay className="h-6 w-6 text-[#4ECDC4]" />,
title: "Built by Developers",
description:
"For developers, by developers. Everything you need to build and test with ease.",
link: {
text: "Development Guide",
url: constants.env.DOCS_URL,
},
},
];
export const Features = ({ features }: { features: Feature[] }) => (
<section className="container mx-auto px-4 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => (
<div
key={index}
className="relative group overflow-hidden bg-[#1a1a1a] p-6 rounded-lg border border-gray-800 transition-all duration-300 ease-in-out hover:shadow-lg hover:shadow-[#4ECDC4]/10
<section className="container mx-auto px-4 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => (
<div
key={index}
className="relative group overflow-hidden bg-[#1a1a1a] p-6 rounded-lg border border-gray-800 transition-all duration-300 ease-in-out hover:shadow-lg hover:shadow-[#4ECDC4]/10
before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite]
before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent"
>
<div className="relative z-10">
<div className="bg-[#252525] w-12 h-12 rounded-lg flex items-center justify-center mb-4">
{feature.icon}
</div>
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
<p className="text-gray-400">{feature.description}</p>
{feature.link && (
<Link
href={feature.link.url}
className="text-[#4ECDC4] hover:underline flex items-center gap-1 mt-4 text-sm"
>
{feature.link.text}{" "}
<ChevronDown className="h-4 w-4 rotate-270" />
</Link>
)}
</div>
</div>
))}
</div>
</section>
)
>
<div className="relative z-10">
<div className="bg-[#252525] w-12 h-12 rounded-lg flex items-center justify-center mb-4">
{feature.icon}
</div>
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
<p className="text-gray-400">{feature.description}</p>
{feature.link && (
<Link
href={feature.link.url}
className="text-[#4ECDC4] hover:underline flex items-center gap-1 mt-4 text-sm"
>
{feature.link.text}{" "}
<ChevronDown className="h-4 w-4 rotate-270" />
</Link>
)}
</div>
</div>
))}
</div>
</section>
);

View File

@@ -1,57 +1,57 @@
"use client"
"use client";
import Link from "next/link"
import { constants } from "@/constants"
import Image from "next/image"
import Link from "next/link";
import { constants } from "@/constants";
import Image from "next/image";
export const Footer = () => (
<footer className="border-t border-gray-800 py-8 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col items-center justify-center gap-6">
<div className="flex items-center">
<span className="text-white font-bold">Letter</span>
<span className="text-primary font-bold">Space</span>
<span className="text-gray-400 text-sm ml-4">
©{new Date().getFullYear()} LetterSpace
</span>
</div>
<div className="flex gap-6">
<Link
href={constants.env.DOCS_URL}
className="text-gray-400 hover:text-white"
>
Documentation
</Link>
<Link
href={constants.env.GITHUB_URL}
className="text-gray-400 hover:text-white"
target="_blank"
>
GitHub
</Link>
<Link
href={constants.env.DOCS_URL + "/api"}
className="text-gray-400 hover:text-white"
>
API
</Link>
<Link
href={constants.env.X_URL}
className="text-gray-400 hover:text-white flex items-center gap-1.5"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="/x.svg"
alt="X"
width={20}
height={20}
style={{ filter: "invert(1)" }}
/>
Built by dcodes
</Link>
</div>
</div>
</div>
</footer>
)
<footer className="border-t border-gray-800 py-8 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col items-center justify-center gap-6">
<div className="flex items-center">
<span className="text-white font-bold">Letter</span>
<span className="text-primary font-bold">Space</span>
<span className="text-gray-400 text-sm ml-4">
©{new Date().getFullYear()} LetterSpace
</span>
</div>
<div className="flex gap-6">
<Link
href={constants.env.DOCS_URL}
className="text-gray-400 hover:text-white"
>
Documentation
</Link>
<Link
href={constants.env.GITHUB_URL}
className="text-gray-400 hover:text-white"
target="_blank"
>
GitHub
</Link>
<Link
href={constants.env.DOCS_URL + "/api"}
className="text-gray-400 hover:text-white"
>
API
</Link>
<Link
href={constants.env.X_URL}
className="text-gray-400 hover:text-white flex items-center gap-1.5"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="/x.svg"
alt="X"
width={20}
height={20}
style={{ filter: "invert(1)" }}
/>
Built by dcodes
</Link>
</div>
</div>
</div>
</footer>
);

View File

@@ -1,47 +1,47 @@
"use client"
"use client";
import Link from "next/link"
import { Menu } from "lucide-react"
import { Button } from "@repo/ui"
import { constants } from "@/constants"
import Image from "next/image"
import Link from "next/link";
import { Menu } from "lucide-react";
import { Button } from "@repo/ui";
import { constants } from "@/constants";
import Image from "next/image";
export const Header = () => (
<header className="border-b border-gray-800">
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/" className="flex items-center font-bold text-xl">
<span className="text-white">Letter</span>
<span className="text-primary">Space</span>
</Link>
</div>
<header className="border-b border-gray-800">
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/" className="flex items-center font-bold text-xl">
<span className="text-white">Letter</span>
<span className="text-primary">Space</span>
</Link>
</div>
<nav className="hidden md:flex items-center gap-6">
<Link
href="#"
className="text-gray-300 hover:text-white flex items-center gap-1"
>
v{constants.version}
</Link>
<Link
href={constants.env.GITHUB_URL}
className="text-gray-300 hover:text-white"
target="_blank"
>
<Image
src="/github.svg"
alt="GitHub"
width={30}
height={30}
style={{ filter: "invert(1)" }}
/>
</Link>
</nav>
<nav className="hidden md:flex items-center gap-6">
<Link
href="#"
className="text-gray-300 hover:text-white flex items-center gap-1"
>
v{constants.version}
</Link>
<Link
href={constants.env.GITHUB_URL}
className="text-gray-300 hover:text-white"
target="_blank"
>
<Image
src="/github.svg"
alt="GitHub"
width={30}
height={30}
style={{ filter: "invert(1)" }}
/>
</Link>
</nav>
<Button variant="ghost" size="icon" className="md:hidden">
<span className="sr-only">Open menu</span>
<Menu className="h-6 w-6" />
</Button>
</div>
</header>
)
<Button variant="ghost" size="icon" className="md:hidden">
<span className="sr-only">Open menu</span>
<Menu className="h-6 w-6" />
</Button>
</div>
</header>
);

View File

@@ -1,118 +1,118 @@
"use client"
"use client";
import constants from "@/constants"
import { Button } from "@repo/ui"
import { Mail, Star, Zap } from "lucide-react"
import Link from "next/link"
import constants from "@/constants";
import { Button } from "@repo/ui";
import { Mail, Star, Zap } from "lucide-react";
import Link from "next/link";
export const Hero = () => (
<section className="container mx-auto px-4 py-16 md:py-24 flex flex-col md:flex-row items-center justify-between">
<div className="md:w-1/2 space-y-4">
<h2 className="text-3xl font-bold">
Letter<span className="text-primary">Space</span>
</h2>
<h1 className="text-5xl md:text-6xl font-bold text-white">
Open Source Email Platform
</h1>
<div className="space-y-2 max-w-xl">
<p className="text-gray-400 text-xl">
Connect any SMTP server and manage unlimited newsletters
</p>
<p className="text-gray-400 text-xl">
Zero-config and developer-friendly
</p>
<p className="text-gray-400 text-xl">LetterSpace for everyone</p>
</div>
<section className="container mx-auto px-4 py-16 md:py-24 flex flex-col md:flex-row items-center justify-between">
<div className="md:w-1/2 space-y-4">
<h2 className="text-3xl font-bold">
Letter<span className="text-primary">Space</span>
</h2>
<h1 className="text-5xl md:text-6xl font-bold text-white">
Open Source Email Platform
</h1>
<div className="space-y-2 max-w-xl">
<p className="text-gray-400 text-xl">
Connect any SMTP server and manage unlimited newsletters
</p>
<p className="text-gray-400 text-xl">
Zero-config and developer-friendly
</p>
<p className="text-gray-400 text-xl">LetterSpace for everyone</p>
</div>
<div className="flex gap-4 pt-4">
<Link href={constants.env.DOCS_URL}>
<Button className="bg-[#4ECDC4] hover:bg-[#3DBDB5] text-black font-medium px-6">
Get Started
</Button>
</Link>
<Link href={constants.env.GITHUB_URL}>
<Button
variant="outline"
className="border-gray-700 text-gray-300 hover:text-white hover:bg-gray-800 bg-gray-900"
>
View on GitHub
</Button>
</Link>
</div>
</div>
<div className="flex gap-4 pt-4">
<Link href={constants.env.DOCS_URL}>
<Button className="bg-[#4ECDC4] hover:bg-[#3DBDB5] text-black font-medium px-6">
Get Started
</Button>
</Link>
<Link href={constants.env.GITHUB_URL}>
<Button
variant="outline"
className="border-gray-700 text-gray-300 hover:text-white hover:bg-gray-800 bg-gray-900"
>
View on GitHub
</Button>
</Link>
</div>
</div>
<div className="md:w-1/2 flex justify-center mt-12 md:mt-0">
<div className="relative w-80 h-80">
{/* Space background with stars */}
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-[#1a1a1a] to-[#121212] overflow-hidden">
{/* Stars */}
<div className="absolute inset-0">
<div className="star star-1"></div>
<div className="star star-2"></div>
<div className="star star-3"></div>
<div className="star star-4"></div>
<div className="star star-5"></div>
<div className="star star-6"></div>
<div className="star star-7"></div>
<div className="star star-8"></div>
<div className="star star-9"></div>
<div className="star star-10"></div>
<div className="star star-11"></div>
<div className="star star-12"></div>
</div>
<div className="md:w-1/2 flex justify-center mt-12 md:mt-0">
<div className="relative w-80 h-80">
{/* Space background with stars */}
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-[#1a1a1a] to-[#121212] overflow-hidden">
{/* Stars */}
<div className="absolute inset-0">
<div className="star star-1"></div>
<div className="star star-2"></div>
<div className="star star-3"></div>
<div className="star star-4"></div>
<div className="star star-5"></div>
<div className="star star-6"></div>
<div className="star star-7"></div>
<div className="star star-8"></div>
<div className="star star-9"></div>
<div className="star star-10"></div>
<div className="star star-11"></div>
<div className="star star-12"></div>
</div>
{/* Animated nebula/galaxy effect */}
<div className="absolute inset-0 bg-gradient-to-br from-[#4ECDC4]/10 to-transparent blur-2xl animate-pulse"></div>
<div className="absolute inset-0 bg-gradient-to-tr from-yellow-400/5 to-transparent blur-3xl animate-pulse animation-delay-700"></div>
</div>
{/* Animated nebula/galaxy effect */}
<div className="absolute inset-0 bg-gradient-to-br from-[#4ECDC4]/10 to-transparent blur-2xl animate-pulse"></div>
<div className="absolute inset-0 bg-gradient-to-tr from-yellow-400/5 to-transparent blur-3xl animate-pulse animation-delay-700"></div>
</div>
{/* Floating envelope in space */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
{/* Envelope with 3D perspective */}
<div className="relative w-48 h-36 bg-gradient-to-br from-[#4ECDC4] to-[#2A9D94] rounded-lg shadow-lg transform rotate-6 animate-float-space perspective">
{/* Envelope flap */}
<div className="absolute top-0 left-0 w-full h-1/3 bg-gradient-to-b from-[#5DDED6] to-[#4ECDC4] rounded-t-lg envelope-flap"></div>
{/* Floating envelope in space */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
{/* Envelope with 3D perspective */}
<div className="relative w-48 h-36 bg-gradient-to-br from-[#4ECDC4] to-[#2A9D94] rounded-lg shadow-lg transform rotate-6 animate-float-space perspective">
{/* Envelope flap */}
<div className="absolute top-0 left-0 w-full h-1/3 bg-gradient-to-b from-[#5DDED6] to-[#4ECDC4] rounded-t-lg envelope-flap"></div>
{/* Envelope body */}
<div className="absolute bottom-0 left-0 w-full h-2/3 bg-[#4ECDC4] rounded-b-lg border-t border-white/20"></div>
{/* Envelope body */}
<div className="absolute bottom-0 left-0 w-full h-2/3 bg-[#4ECDC4] rounded-b-lg border-t border-white/20"></div>
{/* Letter peeking out */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-5/6 h-3/4 bg-white rounded-sm shadow-inner transform -translate-y-2 letter">
<div className="w-full h-full p-2 flex flex-col justify-center">
<div className="w-full h-2 bg-gray-300 rounded-full mb-2"></div>
<div className="w-3/4 h-2 bg-gray-300 rounded-full mb-2"></div>
<div className="w-5/6 h-2 bg-gray-300 rounded-full"></div>
</div>
</div>
</div>
{/* Letter peeking out */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-5/6 h-3/4 bg-white rounded-sm shadow-inner transform -translate-y-2 letter">
<div className="w-full h-full p-2 flex flex-col justify-center">
<div className="w-full h-2 bg-gray-300 rounded-full mb-2"></div>
<div className="w-3/4 h-2 bg-gray-300 rounded-full mb-2"></div>
<div className="w-5/6 h-2 bg-gray-300 rounded-full"></div>
</div>
</div>
</div>
{/* Orbiting elements */}
<div className="absolute w-full h-full animate-orbit">
<div className="absolute -right-4 -top-8">
<Star className="h-6 w-6 text-yellow-400 animate-twinkle" />
</div>
</div>
{/* Orbiting elements */}
<div className="absolute w-full h-full animate-orbit">
<div className="absolute -right-4 -top-8">
<Star className="h-6 w-6 text-yellow-400 animate-twinkle" />
</div>
</div>
<div className="absolute w-full h-full animate-orbit-reverse">
<div className="absolute -left-8 top-0">
<Mail className="h-8 w-8 text-[#4ECDC4] animate-pulse" />
</div>
</div>
<div className="absolute w-full h-full animate-orbit-reverse">
<div className="absolute -left-8 top-0">
<Mail className="h-8 w-8 text-[#4ECDC4] animate-pulse" />
</div>
</div>
<div className="absolute w-full h-full animate-orbit-slow">
<div className="absolute right-0 bottom-0">
<Zap className="h-10 w-10 text-yellow-400 filter drop-shadow-lg" />
</div>
</div>
</div>
<div className="absolute w-full h-full animate-orbit-slow">
<div className="absolute right-0 bottom-0">
<Zap className="h-10 w-10 text-yellow-400 filter drop-shadow-lg" />
</div>
</div>
</div>
{/* Floating particles */}
<div className="particle particle-1"></div>
<div className="particle particle-2"></div>
<div className="particle particle-3"></div>
<div className="particle particle-4"></div>
<div className="particle particle-5"></div>
</div>
</div>
</section>
)
{/* Floating particles */}
<div className="particle particle-1"></div>
<div className="particle particle-2"></div>
<div className="particle particle-3"></div>
<div className="particle particle-4"></div>
<div className="particle particle-5"></div>
</div>
</div>
</section>
);

View File

@@ -1,18 +1,18 @@
import { APP_VERSION } from "@repo/shared"
import { APP_VERSION } from "@repo/shared";
export const constants = {
env: {
DOCS_URL: process.env.NEXT_PUBLIC_DOCS_URL!,
GITHUB_URL: process.env.NEXT_PUBLIC_GITHUB_URL!,
X_URL: "https://x.com/dcodesdev",
},
version: APP_VERSION,
}
env: {
DOCS_URL: process.env.NEXT_PUBLIC_DOCS_URL!,
GITHUB_URL: process.env.NEXT_PUBLIC_GITHUB_URL!,
X_URL: "https://x.com/dcodesdev",
},
version: APP_VERSION,
};
Object.entries(constants.env).forEach(([key, value]) => {
if (!value) {
throw new Error(`${key} is not defined`)
}
})
if (!value) {
throw new Error(`${key} is not defined`);
}
});
export default constants
export default constants;