Compare commits

...

3 Commits

8 changed files with 288 additions and 169 deletions

2
.gitignore vendored
View File

@ -52,5 +52,5 @@ bun.lock
bun.lockb bun.lockb
# prisma # prisma
prisma/dev.db prisma/dev.db*
prisma/migrations/ prisma/migrations/

View File

@ -218,6 +218,6 @@ npx prisma migrate deploy # Deploy
## To-Do ## To-Do
* [X] Add documentation on .env * [ ] Add theme switcher to home page
* [ ] Implement security scans * [ ] Implement security scans
* [ ] Rate-limiting on API * [ ] Rate-limiting on API

View File

@ -1,7 +1,5 @@
import Hero from "@/components/pages/main/Hero" import Hero from "@/components/pages/main/Hero"
import FeatureCard from "@/components/pages/main/FeatureCard" import FeatureCard from "@/components/pages/main/FeatureCard"
import { Mail, Lock, Disc3, Headset } from "lucide-react"
import { SiGitea, SiAuthentik } from "react-icons/si";
import Navbar from "@/components/pages/main/Navbar" import Navbar from "@/components/pages/main/Navbar"
import Footer from "@/components/pages/main/Footer" import Footer from "@/components/pages/main/Footer"
import PoweredBySection from "@/components/pages/main/PoweredBySection" import PoweredBySection from "@/components/pages/main/PoweredBySection"
@ -12,34 +10,34 @@ export default function Home() {
{ {
title: "Email", title: "Email",
description: "Free email service with webmail and antispam, powered by a custom docker-mailserver setup.", description: "Free email service with webmail and antispam, powered by a custom docker-mailserver setup.",
icon: Mail, iconName: "Mail",
}, },
{ {
title: "Password Manager", title: "Password Manager",
description: "Securely store and manage your passwords across devices with Vaultwarden.", description: "Securely store and manage your passwords across devices with Vaultwarden.",
icon: Lock, iconName: "Lock",
}, },
{ {
title: "Git", title: "Git",
description: "Host your repositories and run actions free of charge on our Gitea instance.", description: "Host your repositories and run actions free of charge on our Gitea instance.",
icon: SiGitea, iconName: "SiGitea",
}, },
{ {
title: "Authentik", title: "Authentik",
description: "A secure single-sign-on service for easy login to your other services.", description: "A secure single-sign-on service for easy login to your other services.",
icon: SiAuthentik, iconName: "SiAuthentik",
}, },
{ {
title: "Music", title: "Music",
description: "Coming soon. Host your music on our community server and stream it everywhere", description: "Coming soon. Host your music on our community server and stream it everywhere",
icon: Disc3, iconName: "Disc3",
}, },
{ {
title: "Support", title: "Support",
description: "Administrators are standing by most of the day via our various support channels.", description: "Administrators are standing by most of the day via our various support channels.",
icon: Headset, iconName: "Headset",
}, },
] ] as const
return ( return (
<div className="min-h-screen dark:bg-linear-to-b dark:from-gray-950 dark:to-gray-900"> <div className="min-h-screen dark:bg-linear-to-b dark:from-gray-950 dark:to-gray-900">

View File

@ -0,0 +1,35 @@
"use client"
import { motion } from "framer-motion"
import React from "react"
import { Mail, Lock, Disc3, Headset } from "lucide-react"
import { SiGitea, SiAuthentik } from "react-icons/si"
const iconMap = {
Mail,
Lock,
Disc3,
Headset,
SiGitea,
SiAuthentik,
} as const
interface AnimatedIconProps {
iconName: keyof typeof iconMap
className?: string
}
const AnimatedIcon = ({ iconName, className }: AnimatedIconProps) => {
const Icon = iconMap[iconName]
return (
<motion.div
whileHover={{ rotate: 5 }}
transition={{ type: "spring", stiffness: 400, damping: 10 }}
>
<Icon className={className} />
</motion.div>
)
}
export default AnimatedIcon

View File

@ -1,28 +1,44 @@
"use client"
import React from "react" import React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { motion } from "framer-motion"
import AnimatedIcon from "./AnimatedIcon"
interface FeatureCardProps { interface FeatureCardProps {
title: string title: string
description: string description: string
icon: React.ElementType iconName: Parameters<typeof AnimatedIcon>[0]["iconName"]
} }
/* TODO: I plan to add a better animation in the future, hover effects are not const FeatureCard = ({ title, description, iconName }: FeatureCardProps) => {
good here, in my opinion. */
const FeatureCard = ({ title, description, icon: Icon }: FeatureCardProps) => {
return ( return (
<Card className="bg-background border-accent transition-colors duration-300"> <motion.div
whileHover={{
scale: 1.01,
y: -2,
transition: {
type: "spring",
stiffness: 400,
damping: 25
}
}}
>
<Card className="bg-card text-card-foreground border-border hover:bg-accent/5 relative group transition-colors duration-300">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center"> <CardTitle className="flex items-center">
<Icon className="h-6 w-6 mr-2 text-blue-400" /> <AnimatedIcon
<span className="text-xl">{title}</span> iconName={iconName}
className="h-6 w-6 mr-2 text-primary group-hover:text-primary/80 transition-colors duration-300"
/>
<span className="text-xl text-foreground group-hover:text-foreground/90 transition-colors duration-300">{title}</span>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<CardDescription>{description}</CardDescription> <CardDescription className="text-muted-foreground group-hover:text-muted-foreground/90 transition-colors duration-300">{description}</CardDescription>
</CardContent> </CardContent>
</Card> </Card>
</motion.div>
) )
} }

View File

@ -1,18 +1,69 @@
"use client" "use client"
import type React from "react" import type React from "react"
import { useState } from "react"
import { Check, ChevronRight, Clock } from "lucide-react" import { Check, ChevronRight, Clock } from "lucide-react"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { motion } from "framer-motion"
import Link from "next/link" import Link from "next/link"
import { motion, HTMLMotionProps } from "framer-motion"
export default function Pricing() { interface PricingFeatures {
const [hoveredCard, setHoveredCard] = useState<number | null>(null) everything: string[]
storage: string[]
ai: string[]
}
const features = { interface FeatureItemProps {
children: React.ReactNode
}
interface PricingCardProps {
title: string
price: string
period: string
description: string
features: string[]
badge?: string
buttonText?: string
buttonIcon?: React.ReactNode
isComingSoon?: boolean
className?: string
}
const cardVariants = {
initial: {
transform: "scale(1)",
transition: {
type: "spring",
stiffness: 300,
damping: 20,
layout: false
}
},
hover: {
transform: "scale(1.02)",
transition: {
type: "spring",
stiffness: 300,
damping: 20,
layout: false
}
}
} as const
const textVariants = {
initial: {
color: "inherit",
transition: { duration: 0.2 }
},
hover: {
color: "inherit",
transition: { duration: 0.2 }
}
} as const
const features: PricingFeatures = {
everything: [ everything: [
"Use anything and everything on LibreCloud", "Use anything and everything on LibreCloud",
"Unlimited Password/Secret Storage with Vaultwarden", "Unlimited Password/Secret Storage with Vaultwarden",
@ -32,24 +83,121 @@ export default function Pricing() {
"Flexible options such as OpenRouter and Cloud VPS", "Flexible options such as OpenRouter and Cloud VPS",
"Pay per token or hour at no additional cost from LibreCloud", "Pay per token or hour at no additional cost from LibreCloud",
"Use our open-source chat interface with a range of providers", "Use our open-source chat interface with a range of providers",
"Use the latest models such as Anthropic's Claude 3.7 Sonnet and OpenAI's o3-mini" "Use the latest models such as Anthropic's Claude 3.7 Sonnet and OpenAI's o3-mini",
], ],
} }
const FeatureItem = ({ children }: { children: React.ReactNode }) => ( const FeatureItem: React.FC<FeatureItemProps> = ({ children }) => (
<div className="flex items-start space-x-2 mb-4"> <div className="flex items-start space-x-2 mb-4">
<Check className="h-5 w-5 text-primary shrink-0 mt-0.5" /> <div className="flex-shrink-0 flex items-center justify-center">
<span className="text-sm text-foreground">{children}</span> <Check className="h-5 w-5 text-primary" />
</div>
<motion.span
initial="initial"
whileHover="hover"
variants={textVariants}
className="text-sm text-muted-foreground group-hover:text-muted-foreground/90 transition-colors duration-300"
>
{children}
</motion.span>
</div> </div>
) )
const cardVariants = { const PricingCard: React.FC<PricingCardProps> = ({
default: { scale: 1, y: 0 }, title,
hover: { scale: 1.03, y: -8 }, price,
} period,
description,
features,
badge,
buttonText,
buttonIcon,
isComingSoon,
className,
}) => (
<motion.div
className="relative h-full"
initial="initial"
whileHover="hover"
variants={cardVariants}
style={{
willChange: "transform",
transformStyle: "preserve-3d"
}}
>
<div
className={`relative h-full overflow-hidden rounded-xl border border-border bg-card text-card-foreground hover:bg-accent/5 transition-colors duration-300 group ${className}`}
>
<div className="p-6">
<div className="mb-4 flex items-center justify-between">
<motion.h3
variants={textVariants}
className="text-2xl font-bold text-foreground group-hover:text-foreground/90 transition-colors duration-300"
>
{title}
</motion.h3>
{badge && (
<div>
<Badge variant="outline" className="text-xs font-medium bg-background/50 text-foreground/90">
{badge}
</Badge>
</div>
)}
</div>
<div>
<motion.span
variants={textVariants}
className="text-6xl font-bold text-foreground group-hover:text-foreground/90 transition-colors duration-300"
>
{price}
</motion.span>
<motion.span
variants={textVariants}
className="text-muted-foreground ml-2 group-hover:text-muted-foreground/90 transition-colors duration-300"
>
{period}
</motion.span>
</div>
<motion.p
variants={textVariants}
className="text-sm text-muted-foreground mt-4 mb-6 group-hover:text-muted-foreground/90 transition-colors duration-300"
>
{description}
</motion.p>
{isComingSoon ? (
<Button className="w-full" size="lg" variant="outline" disabled>
<Clock className="mr-2" /> Coming Soon
</Button>
) : buttonText ? (
<Link href="/account/login" className="block">
<Button className="w-full group" size="lg">
{buttonText}
{buttonIcon}
</Button>
</Link>
) : null}
</div>
<Separator className="bg-border" />
<div className="p-6 space-y-4">
<motion.h4
variants={textVariants}
className="text-sm font-medium text-foreground group-hover:text-foreground/90 transition-colors duration-300"
>
What&#39;s included:
</motion.h4>
<div className="space-y-1">
{features.map((feature, index) => (
<FeatureItem key={index}>{feature}</FeatureItem>
))}
</div>
</div>
</div>
</motion.div>
)
export default function Pricing(): React.ReactElement {
return ( return (
<section id="pricing" className="pb-10"> <section id="pricing" className="pb-10 relative overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-4xl font-extrabold text-foreground mb-4">Pricing You&#39;ll Love</h2> <h2 className="text-4xl font-extrabold text-foreground mb-4">Pricing You&#39;ll Love</h2>
@ -59,112 +207,34 @@ export default function Pricing() {
</div> </div>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
<motion.div <PricingCard
className={`relative overflow-hidden rounded-xl border bg-slate-200 dark:bg-gray-800 text-card-foreground shadow transition-all duration-300 ${hoveredCard === 0 ? "border-gray-700" : "border-border"} text-background`} title="Everything"
variants={cardVariants} price="$0.00"
initial="default" period="/mo"
animate={hoveredCard === 0 ? "hover" : "default"} description="All the services we offer, completely free."
onMouseEnter={() => setHoveredCard(0)} features={features.everything}
onMouseLeave={() => setHoveredCard(null)} badge="Most Popular"
> buttonText="Get Started"
<div className="p-6"> buttonIcon={<ChevronRight className="ml-2 h-4 w-4" />}
<div className="mb-4 flex items-center justify-between"> />
<h3 className="text-2xl font-bold">Everything</h3>
<Badge variant="outline" className="text-xs font-medium bg-background">
Most Popular
</Badge>
</div>
<div className="mb-4">
<span className="text-6xl font-bold">$0.00</span>
<span className="text-muted-foreground ml-2">/mo</span>
</div>
<p className="text-sm text-secondary-foreground mb-6">
All the services we offer, completely free.
</p>
<Link href="/account/login">
<Button className="w-full" size="lg">
Get Started <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</Link>
</div>
{/* TODO: this seperator be improved in the future, i can't find a good color for this */}
<Separator className="bg-gray-700" />
<div className="p-6 space-y-4">
<h4 className="text-sm font-medium">What&#39;s included:</h4>
<div>
{features.everything.map((feature, index) => (
<FeatureItem key={index}>{feature}</FeatureItem>
))}
</div>
</div>
</motion.div>
<motion.div <PricingCard
className={`relative overflow-hidden rounded-xl border text-card-foreground shadow transition-all duration-300 ${hoveredCard === 1 ? "border-gray-700" : "border-border"}`} title="Storage"
variants={cardVariants} price="$0.117"
initial="default" period="/GB"
animate={hoveredCard === 1 ? "hover" : "default"} description="Flexible storage options with no markup."
onMouseEnter={() => setHoveredCard(1)} features={features.storage}
onMouseLeave={() => setHoveredCard(null)} isComingSoon
> />
<div className="p-6">
<h3 className="text-2xl font-bold mb-2">Storage</h3>
<div className="text-sm text-muted-foreground mb-2">Starting at</div>
<div className="mb-4">
<span className="text-6xl font-bold">$0.117</span>
<span className="text-muted-foreground ml-2">/GB</span>
</div>
<p className="text-sm text-muted-foreground mb-6">Flexible storage options with no markup.</p>
<Button className="w-full" size="lg" variant="outline" disabled>
<Clock /> Coming Soon
</Button>
</div>
<Separator />
<div className="p-6 space-y-4">
<h4 className="text-sm font-medium">What&#39;s included:</h4>
<div>
{features.storage.map((feature, index) => (
<FeatureItem key={index}>{feature}</FeatureItem>
))}
</div>
<div className="pt-4">
<p className="text-xs text-muted-foreground">
Calculator price rounded to nearest dollar and based on ElcroDigital provider
</p>
</div>
</div>
</motion.div>
<motion.div <PricingCard
className={`relative overflow-hidden rounded-xl border text-card-foreground shadow transition-all duration-300 ${hoveredCard === 2 ? "border-gray-700" : "border-border"}`} title="Generative AI"
variants={cardVariants} price="$0.00"
initial="default" period="/M tokens"
animate={hoveredCard === 2 ? "hover" : "default"} description="Access powerful AI models at the best price."
onMouseEnter={() => setHoveredCard(2)} features={features.ai}
onMouseLeave={() => setHoveredCard(null)} isComingSoon
> />
<div className="p-6">
<h3 className="text-2xl font-bold mb-2">Generative AI</h3>
<div className="text-sm text-muted-foreground mb-2">Starting at</div>
<div className="mb-4">
<span className="text-6xl font-bold">$0.00</span>
<span className="text-muted-foreground ml-2">/M tokens</span>
</div>
<p className="text-sm text-muted-foreground mb-6">Access powerful AI models at the best price.</p>
<Button className="w-full" size="lg" variant="outline" disabled>
<Clock /> Coming Soon
</Button>
</div>
<Separator />
<div className="p-6 space-y-4">
<h4 className="text-sm font-medium">What&#39;s included:</h4>
<div>
{features.ai.map((feature, index) => (
<FeatureItem key={index}>{feature}</FeatureItem>
))}
</div>
</div>
</motion.div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -3,7 +3,7 @@ services:
build: . build: .
ports: ports:
- "3019:3000" - "3019:3000"
env_file: ".env" env_file: ".env.local"
volumes: volumes:
- ./prisma:/app/prisma - ./prisma:/app/prisma
environment: environment:

View File

@ -29,10 +29,10 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.0", "cmdk": "1.0.0",
"cookies-next": "^5.1.0", "cookies-next": "^5.1.0",
"framer-motion": "^12.6.2",
"geist": "^1.3.1", "geist": "^1.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"motion": "^12.6.2",
"next": "^15.2.4", "next": "^15.2.4",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",