Merge pull request #3 from hex248/development

Marketing ready landing page
This commit is contained in:
Oliver Bryan
2026-01-27 21:16:38 +00:00
committed by GitHub
10 changed files with 592 additions and 138 deletions

View File

@@ -1,3 +1 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="256" cy="256" r="192" stroke="#FFFFFF" stroke-width="32" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 24 24" class="" color="#F26D77"><path d="M10 3H8v2H6v2h2V5h2v2h2v2h-2v2H8v2H6v2H4v-2H2v2h2v2h2v-2h4v2h2v2h-2v2h2v-2h2v-2h-2v-4h2v-2h2v2h2v2h2v-2h2v-2h-2v2h-2v-2h-2V9h2V5h-4v2h-2V5h-2V3z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -114,7 +114,7 @@ export function ServerConfiguration({ trigger }: { trigger?: ReactNode }) {
<DialogTrigger asChild>
{trigger || (
<IconButton size="lg" className="absolute top-2 right-2" title={"Server Configuration"}>
<Icon icon="serverIcon" className="size-4" />
<Icon icon="server" className="size-4" />
</IconButton>
)}
</DialogTrigger>

View File

@@ -2,6 +2,7 @@ import {
Alert as PixelAlert,
Check as PixelCheck,
Checkbox as PixelCheckbox,
CheckboxOn as PixelCheckboxOn,
ChevronDown as PixelChevronDown,
ChevronLeft as PixelChevronLeft,
ChevronRight as PixelChevronRight,
@@ -10,8 +11,14 @@ import {
Clock as PixelClock,
Close as PixelClose,
MessagePlus as PixelCommentSend,
CreditCard as PixelCreditCard,
CreditCardDelete as PixelCreditCardDelete,
Dashboard as PixelDashboard,
Debug as PixelDebug,
DebugOff as PixelDebugOff,
Edit as PixelEdit,
EyeClosed as PixelEyeClosed,
AddGrid as PixelGridAdd,
Home as PixelHome,
InfoBox as PixelInfo,
Link as PixelLink,
@@ -24,14 +31,18 @@ import {
Play as PixelPlay,
Plus as PixelPlus,
Server as PixelServer,
Shield as PixelShield,
Ship as PixelShip,
CheckboxOn as PixelStop,
Sun as PixelSun,
Trash as PixelTrash,
Undo as PixelUndo,
User as PixelUser,
ViewportWide as PixelViewportWide,
Zap as PixelZap,
} from "@nsmr/pixelart-react";
import {
ArrowCounterClockwiseIcon as PhosphorArrowCounterClockwise,
BugIcon as PhosphorBug,
CheckIcon as PhosphorCheck,
CheckCircleIcon as PhosphorCheckCircle,
@@ -41,15 +52,19 @@ import {
CaretRightIcon as PhosphorChevronRight,
CaretUpIcon as PhosphorChevronUp,
CircleIcon as PhosphorCircle,
ClockIcon as PhosphorClock,
ChatTextIcon as PhosphorComment,
CreditCardIcon as PhosphorCreditCard,
CubeIcon as PhosphorCube,
DotsSixVerticalIcon as PhosphorDotsSixVertical,
DotsThreeVerticalIcon as PhosphorDotsThreeVertical,
PencilSimpleIcon as PhosphorEdit,
EyeClosedIcon as PhosphorEyeClosed,
HashIcon as PhosphorHash,
HashStraightIcon as PhosphorHashStraight,
HouseIcon as PhosphorHome,
InfoIcon as PhosphorInfo,
LayoutIcon as PhosphorLayout,
LightningIcon as PhosphorLightning,
LinkIcon as PhosphorLink,
SpinnerGapIcon as PhosphorLoader,
SignOutIcon as PhosphorLogOut,
@@ -59,11 +74,14 @@ import {
PlayIcon as PhosphorPlay,
PlusIcon as PhosphorPlus,
QuestionIcon as PhosphorQuestion,
RocketLaunchIcon as PhosphorRocketLaunch,
HardDrivesIcon as PhosphorServer,
ShieldCheckIcon as PhosphorShieldCheck,
StackPlusIcon as PhosphorStackPlus,
StopIcon as PhosphorStop,
SunIcon as PhosphorSun,
TimerIcon as PhosphorTimer,
TrashIcon as PhosphorTrash,
ArrowCounterClockwiseIcon as PhosphorUndo,
UserIcon as PhosphorUser,
WarningIcon as PhosphorWarning,
XIcon as PhosphorX,
@@ -71,7 +89,9 @@ import {
import type { IconStyle } from "@sprint/shared";
import {
AlertTriangle,
Box,
Bug,
BugOff,
Check,
CheckIcon,
ChevronDown,
@@ -79,15 +99,18 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronUp,
ChevronUpIcon,
CircleCheckIcon,
CircleIcon,
CircleQuestionMark,
CreditCard,
Edit,
EllipsisVertical,
EyeClosed,
Grid2x2Plus as GridAdd,
GripVerticalIcon,
Hash,
InfoIcon,
LayoutDashboard,
Link,
Loader,
Loader2Icon,
@@ -99,17 +122,22 @@ import {
Pause,
Play,
Plus,
Rocket,
RotateCcw,
ServerIcon,
ShieldCheck,
Square,
SquareCheck,
Sun,
Timer,
TimerOff,
Trash,
TriangleAlertIcon,
Undo,
Undo2,
UserRound,
X,
Zap,
} from "lucide-react";
import { useSessionSafe } from "@/components/session-provider";
@@ -118,7 +146,9 @@ import { useSessionSafe } from "@/components/session-provider";
// phosphor: https://phosphoricons.com/
const icons = {
alertTriangle: { lucide: AlertTriangle, pixel: PixelAlert, phosphor: PhosphorWarning },
box: { lucide: Box, pixel: PixelCheckboxOn, phosphor: PhosphorCube },
bug: { lucide: Bug, pixel: PixelDebug, phosphor: PhosphorBug },
bugOff: { lucide: BugOff, pixel: PixelDebugOff, phosphor: PhosphorBug },
check: { lucide: Check, pixel: PixelCheck, phosphor: PhosphorCheck },
checkIcon: { lucide: CheckIcon, pixel: PixelCheck, phosphor: PhosphorCheck },
checkBox: { lucide: SquareCheck, pixel: PixelCheckbox, phosphor: PhosphorCheckSquare },
@@ -127,17 +157,20 @@ const icons = {
chevronLeftIcon: { lucide: ChevronLeftIcon, pixel: PixelChevronLeft, phosphor: PhosphorChevronLeft },
chevronRightIcon: { lucide: ChevronRightIcon, pixel: PixelChevronRight, phosphor: PhosphorChevronRight },
chevronUp: { lucide: ChevronUp, pixel: PixelChevronUp, phosphor: PhosphorChevronUp },
chevronUpIcon: { lucide: ChevronUpIcon, pixel: PixelChevronUp, phosphor: PhosphorChevronUp },
circleCheckIcon: { lucide: CircleCheckIcon, pixel: PixelCheck, phosphor: PhosphorCheckCircle },
circleCheck: { lucide: CircleCheckIcon, pixel: PixelCheck, phosphor: PhosphorCheckCircle },
circleIcon: { lucide: CircleIcon, pixel: PixelCircle, phosphor: PhosphorCircle },
circleQuestionMark: { lucide: CircleQuestionMark, pixel: PixelNoteDelete, phosphor: PhosphorQuestion },
comment: { lucide: MessageSquarePlus, pixel: PixelCommentSend, phosphor: PhosphorComment },
creditCard: { lucide: CreditCard, pixel: PixelCreditCard, phosphor: PhosphorCreditCard },
creditCardDelete: { lucide: CreditCard, pixel: PixelCreditCardDelete, phosphor: PhosphorCreditCard },
edit: { lucide: Edit, pixel: PixelEdit, phosphor: PhosphorEdit },
ellipsisVertical: {
lucide: EllipsisVertical,
pixel: PixelMoreVertical,
phosphor: PhosphorDotsThreeVertical,
},
eyeClosed: { lucide: EyeClosed, pixel: PixelEyeClosed, phosphor: PhosphorEyeClosed },
gridAdd: { lucide: GridAdd, pixel: PixelGridAdd, phosphor: PhosphorStackPlus },
gripVerticalIcon: {
lucide: GripVerticalIcon,
pixel: PixelViewportWide,
@@ -145,26 +178,32 @@ const icons = {
},
hash: { lucide: Hash, pixel: PhosphorHashStraight, phosphor: PhosphorHash },
home: { lucide: LucideHome, pixel: PixelHome, phosphor: PhosphorHome },
infoIcon: { lucide: InfoIcon, pixel: PixelInfo, phosphor: PhosphorInfo },
info: { lucide: InfoIcon, pixel: PixelInfo, phosphor: PhosphorInfo },
layoutDashboard: { lucide: LayoutDashboard, pixel: PixelDashboard, phosphor: PhosphorLayout },
link: { lucide: Link, pixel: PixelLink, phosphor: PhosphorLink },
loader: { lucide: Loader, pixel: PixelLoader, phosphor: PhosphorLoader },
loader2Icon: { lucide: Loader2Icon, pixel: PixelLoader, phosphor: PhosphorLoader },
loader2: { lucide: Loader2Icon, pixel: PixelLoader, phosphor: PhosphorLoader },
logOut: { lucide: LogOut, pixel: PixelLogout, phosphor: PhosphorLogOut },
moon: { lucide: Moon, pixel: PixelMoon, phosphor: PhosphorMoon },
octagonXIcon: { lucide: OctagonXIcon, pixel: PixelClose, phosphor: PhosphorOctagon },
octagonX: { lucide: OctagonXIcon, pixel: PixelClose, phosphor: PhosphorOctagon },
pause: { lucide: Pause, pixel: PixelPause, phosphor: PhosphorPause },
play: { lucide: Play, pixel: PixelPlay, phosphor: PhosphorPlay },
plus: { lucide: Plus, pixel: PixelPlus, phosphor: PhosphorPlus },
serverIcon: { lucide: ServerIcon, pixel: PixelServer, phosphor: PhosphorServer },
rocket: { lucide: Rocket, pixel: PixelShip, phosphor: PhosphorRocketLaunch },
rotateCcw: { lucide: RotateCcw, pixel: PixelUndo, phosphor: PhosphorArrowCounterClockwise },
server: { lucide: ServerIcon, pixel: PixelServer, phosphor: PhosphorServer },
shieldCheck: { lucide: ShieldCheck, pixel: PixelShield, phosphor: PhosphorShieldCheck },
sun: { lucide: Sun, pixel: PixelSun, phosphor: PhosphorSun },
stop: { lucide: Square, pixel: PixelStop, phosphor: PhosphorStop },
timer: { lucide: Timer, pixel: PixelClock, phosphor: PhosphorClock },
timer: { lucide: Timer, pixel: PixelClock, phosphor: PhosphorTimer },
timerOff: { lucide: TimerOff, pixel: PixelClock, phosphor: PhosphorTimer },
trash: { lucide: Trash, pixel: PixelTrash, phosphor: PhosphorTrash },
triangleAlertIcon: { lucide: TriangleAlertIcon, pixel: PixelAlert, phosphor: PhosphorWarning },
undo: { lucide: Undo, pixel: PixelUndo, phosphor: PhosphorUndo },
undo2: { lucide: Undo2, pixel: PixelUndo, phosphor: PhosphorUndo },
triangleAlert: { lucide: TriangleAlertIcon, pixel: PixelAlert, phosphor: PhosphorWarning },
undo: { lucide: Undo, pixel: PixelUndo, phosphor: PhosphorArrowCounterClockwise },
undo2: { lucide: Undo2, pixel: PixelUndo, phosphor: PhosphorArrowCounterClockwise },
userRound: { lucide: UserRound, pixel: PixelUser, phosphor: PhosphorUser },
x: { lucide: X, pixel: PixelClose, phosphor: PhosphorX },
zap: { lucide: Zap, pixel: PixelZap, phosphor: PhosphorLightning },
};
export type IconName = keyof typeof icons;
@@ -202,7 +241,7 @@ export default function Icon({
// lucide fills sillily
if (color && resolvedStyle !== "lucide") {
fill = color;
} else if (resolvedStyle === "pixel" && ["bug", "moon", "hash"].includes(icon)) {
} else if (resolvedStyle === "pixel" && ["bug", "moon", "hash", "bugOff"].includes(icon)) {
fill = "var(--foreground)";
} else if (resolvedStyle === "phosphor") {
fill = "var(--foreground)";

View File

@@ -198,7 +198,7 @@ function SelectScrollUpButton({
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<Icon icon="chevronUpIcon" className="size-4" />
<Icon icon="chevronUp" className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}

View File

@@ -10,11 +10,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <Icon icon="circleCheckIcon" className="size-4" />,
info: <Icon icon="infoIcon" className="size-4" />,
warning: <Icon icon="triangleAlertIcon" className="size-4" />,
error: <Icon icon="octagonXIcon" className="size-4" />,
loading: <Icon icon="loader2Icon" className="size-4 animate-spin" />,
success: <Icon icon="circleCheck" className="size-4" />,
info: <Icon icon="info" className="size-4" />,
warning: <Icon icon="triangleAlert" className="size-4" />,
error: <Icon icon="octagonX" className="size-4" />,
loading: <Icon icon="loader2" className="size-4 animate-spin" />,
}}
style={
{

View File

@@ -5,10 +5,12 @@ import { cn } from "@/lib/utils";
function Switch({
className,
thumbClassName,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default";
thumbClassName?: string;
size?: "sm" | "default" | "lg";
}) {
return (
<SwitchPrimitive.Root
@@ -18,8 +20,8 @@ function Switch({
"peer data-[state=checked]:bg-personality data-[state=unchecked]:bg-input",
"focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80",
"group/switch inline-flex shrink-0 items-center rounded-full border border-transparent",
"outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
"outline-none focus-visible:ring-[3px] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
"data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[size=lg]:h-7 data-[size=lg]:w-12",
className,
)}
{...props}
@@ -27,7 +29,8 @@ function Switch({
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-personality dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
"bg-background dark:data-[state=unchecked]:bg-personality dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=lg]/switch:size-6 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
thumbClassName,
)}
/>
</SwitchPrimitive.Root>

View File

@@ -1,83 +1,497 @@
import { Icon } from "@iconify/react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { useSession } from "@/components/session-provider";
import ThemeToggle from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import Icon from "@/components/ui/icon";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
const pricingTiers = [
{
name: "Starter",
price: "£0",
priceAnnual: "£0",
period: "Free forever",
periodAnnual: "Free forever",
description: "Perfect for side projects and solo developers",
tagline: "For solo devs and small projects",
features: [
"1 organisation (owned or joined)",
"1 project",
"100 issues",
"Up to 5 team members",
"Email support",
],
cta: "Get started free",
ctaLink: "/login",
highlighted: false,
},
{
name: "Pro",
price: "£11.99",
priceAnnual: "£9.99",
period: "per user/month",
periodAnnual: "per user/month",
description: "For growing teams and professionals",
tagline: "Most Popular",
features: [
"Everything in starter",
"Unlimited organisations",
"Unlimited projects",
"Unlimited issues",
"Advanced time tracking & reports",
"Custom issue statuses",
"Priority email support",
],
cta: "Try pro free for 14 days",
ctaLink: "/login",
highlighted: true,
},
];
const faqs = [
{
question: "Can I switch plans?",
answer:
"Yes, you can upgrade or downgrade at any time. Changes take effect immediately, and we'll prorate any charges.",
},
{
question: "Is there a free trial?",
answer: "Yes, pro plan includes a 14-day free trial with full access. No credit card required to start.",
},
{
question: "What payment methods do you accept?",
answer: "We accept all major credit cards.",
},
{
question: "What if I need more users?",
answer:
"Pro plan pricing scales with your team. Add or remove users anytime, and we'll adjust your billing automatically.",
},
{
question: "What happens when my trial ends?",
answer:
"You'll automatically downgrade to the free starter plan. No charges unless you actively upgrade to pro.",
},
{
question: "Can I cancel anytime?",
answer:
"Absolutely. Cancel anytime with no questions asked. You'll keep access until the end of your billing period.",
},
{
question: "Do you offer refunds?",
answer: "Yes, we offer a 30-day money-back guarantee. If Sprint isn't right for you, just let us know.",
},
];
export default function Landing() {
const { user, isLoading } = useSession();
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
return (
<div className="min-h-screen flex flex-col">
<header className="relative flex items-center justify-center p-2 border-b">
<div className="text-3xl font-basteleur font-700">Sprint</div>
<nav className="absolute right-2 flex items-center gap-4">
<div className="min-h-screen flex flex-col" id="top">
<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-md">
<div className="w-full flex h-14 items-center justify-between px-2">
<div className="flex items-center gap-2">
<img src="/favicon.svg" alt="Sprint" className="size-12 -mt-0.5" />
<span className="text-3xl font-basteleur font-700 transition-colors -mt-0.5">Sprint</span>
</div>
<nav className="flex items-center gap-6">
<a
href="#top"
className="hidden md:block text-sm font-500 hover:text-personality transition-colors"
>
Home
</a>
<a
href="#features"
className="hidden md:block text-sm font-500 hover:text-personality transition-colors"
>
Features
</a>
<a
href="#pricing"
className="hidden md:block text-sm font-500 hover:text-personality transition-colors"
>
Pricing
</a>
<a
href="#faq"
className="hidden md:block text-sm font-500 hover:text-personality transition-colors"
>
FAQ
</a>
<div className="flex items-center gap-2">
<ThemeToggle />
{!isLoading && user ? (
<>
{user && (
<h1 className="text-xl font-basteleur font-400">Welcome back {user.name.split(" ")[0]}!</h1>
)}
<Button asChild variant="outline" size="sm">
<Link to="/issues">Open app</Link>
</Button>
</>
) : (
<Button asChild variant="outline" size="sm">
<Link to="/login">Sign in</Link>
</Button>
)}
</div>
</nav>
</div>
</header>
<main className="flex-1 flex flex-col items-center justify-center gap-8">
<div className="max-w-3xl text-center space-y-4">
<h1 className="text-[54px] font-basteleur font-700">Need a snappy project management tool?</h1>
<p className="text-[24px] font-goudy text-muted-foreground">
Build your next project with <span className="font-goudy font-700">Sprint.</span>
</p>
<p className="text-[18px] font-goudy text-muted-foreground font-700">
Sick of Jira? Say hello to your new favorite project management tool.
<main className="flex-1 flex flex-col items-center py-16 pt-14 px-4">
<div className="max-w-6xl w-full space-y-18">
{/* hero section */}
<div className="text-center space-y-8">
<div className="flex justify-center mb-8">
<img src="/favicon.svg" alt="Sprint" className="size-48" />
</div>
<div className="space-y-4">
<h1 className="text-[64px] font-basteleur font-700 leading-tight">
Ship faster without the chaos
</h1>
<p className="text-[24px] text-muted-foreground max-w-3xl mx-auto">
Sprint is project management that stays out of your way. Track issues, manage sprints, and
keep your team movingwithout the Jira headache.
</p>
</div>
<div className="flex flex-col items-center gap-8">
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
{!isLoading && user ? (
<Button asChild size="lg">
<Button asChild size="lg" className="text-lg px-8 py-6">
<Link to="/issues">Open app</Link>
</Button>
) : (
<Button asChild size="lg">
<Link to="/login">Get started</Link>
<>
<Button asChild size="lg" className="text-lg px-8 py-6">
<Link to="/login">Start free trial</Link>
</Button>
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
<a href="#pricing">See pricing</a>
</Button>
</>
)}
</div>
<p className="text-sm text-muted-foreground">No credit card required · Full access for 14 days</p>
</div>
{/* problem section */}
<div className="max-w-4xl mx-auto space-y-16">
<h2 className="text-4xl font-basteleur font-700 text-center">
Tired of spending more time managing Jira than building products?
</h2>
<div className="grid md:grid-cols-3 gap-6">
<div className="space-y-2">
<Icon
icon="timerOff"
iconStyle={"pixel"}
className="size-16 mx-auto"
color={"var(--muted-foreground)"}
/>
<p className="text-center text-muted-foreground">
Wasting hours configuring workflows instead of shipping features
</p>
</div>
<div className="space-y-2">
<Icon
icon="gridAdd"
iconStyle={"pixel"}
className="size-16 mx-auto"
color={"var(--muted-foreground)"}
/>
<p className="text-center text-muted-foreground">
Drowning in features you'll never use while missing the basics
</p>
</div>
<div className="space-y-2">
<Icon
icon="bugOff"
iconStyle={"pixel"}
className="size-16 mx-auto"
color={"var(--muted-foreground)"}
/>
<p className="text-center text-muted-foreground">
The software is full of bugs. Your flow state doesn't stand a chance
</p>
</div>
</div>
</div>
{/* solution section */}
<div id="features" className="max-w-5xl mx-auto space-y-12 scroll-mt-4 border-t pt-24">
<h2 className="text-5xl font-basteleur font-700 text-center">
Everything you need, nothing you don't
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="border p-8 space-y-4">
<Icon
icon="layoutDashboard"
iconStyle={"pixel"}
className="size-10"
color={"var(--personality)"}
/>
<h3 className="text-2xl font-basteleur font-700">See your work at a glance</h3>
<p className="text-muted-foreground text-lg">
Beautiful, intuitive issue tracking with customizable statuses. Organize by projects and
sprints. Find what you need in seconds, not minutes.
</p>
</div>
<div className="border p-8 space-y-4">
<Icon icon="timer" iconStyle={"pixel"} className="size-10" color={"var(--personality)"} />
<h3 className="text-2xl font-basteleur font-700">Track time without thinking</h3>
<p className="text-muted-foreground text-lg">
Built-in time tracking that actually works. Start, pause, resume. See where your time goes
without juggling another tool.
</p>
</div>
<div className="border p-8 space-y-4">
<Icon icon="rocket" iconStyle={"pixel"} className="size-10" color={"var(--personality)"} />
<h3 className="text-2xl font-basteleur font-700">Ship in sprints</h3>
<p className="text-muted-foreground text-lg">
Sprint planning that actually works. Set date ranges, assign issues, track velocity. Keep
your team aligned without the ceremony.
</p>
</div>
<div className="border p-8 space-y-4">
<Icon icon="checkBox" iconStyle={"pixel"} className="size-10" color={"var(--personality)"} />
<h3 className="text-2xl font-basteleur font-700">Only use what you need</h3>
<p className="text-muted-foreground text-lg">
Every feature is optional. Sprints, time tracking, and other modules can be enabled or
disabled individually at the organization level. Keep your interface as minimal as your
workflow.
</p>
</div>
</div>
</div>
{/* features list */}
<div className="max-w-4xl mx-auto border p-8 space-y-6">
<h2 className="text-3xl font-basteleur font-700 text-center">
Built for developers, by a developer
</h2>
<div className="grid md:grid-cols-2 gap-x-8 gap-y-3">
{[
"Organization and project management",
"Customizable issue statuses",
"Sprint planning with date ranges",
"Built-in time tracking",
"Native desktop app (Tauri)",
"Clean, resizable interface",
"Issue assignment and collaboration",
"Individual feature toggles (org level)",
].map((feature) => (
<div key={feature} className="flex items-start gap-2">
<Icon icon="check" iconStyle={"pixel"} className="size-6" color={"var(--personality)"} />
<span>{feature}</span>
</div>
))}
</div>
</div>
{/* pricing section */}
<div
id="pricing"
className="max-w-5xl mx-auto space-y-16 flex flex-col items-center border-t pt-24 scroll-mt-4"
>
<div className="text-center space-y-6">
<h2 className="text-5xl font-basteleur font-700">Simple, transparent pricing</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Choose the plan that fits your team. Scale as you grow.
</p>
{/* billing toggle */}
<div className="flex items-center justify-center gap-4 pt-4">
<button
type="button"
onClick={() => setBillingPeriod("monthly")}
className={cn(
"text-lg transition-colors",
billingPeriod === "monthly" ? "text-foreground font-700" : "text-muted-foreground",
)}
>
monthly
</button>
<Switch
size="lg"
checked={billingPeriod === "annual"}
onCheckedChange={(checked) => setBillingPeriod(checked ? "annual" : "monthly")}
className="bg-border data-[state=checked]:bg-border! data-[state=unchecked]:bg-border!"
thumbClassName="bg-personality dark:bg-personality data-[state=checked]:bg-personality! data-[state=unchecked]:bg-personality!"
aria-label="toggle billing period"
/>
<button
type="button"
onClick={() => setBillingPeriod("annual")}
className={cn(
"text-lg transition-colors",
billingPeriod === "annual" ? "text-foreground font-700" : "text-muted-foreground",
)}
>
annual
</button>
<span className="text-sm px-3 py-1 bg-personality/10 text-personality rounded-full font-600">
Save 17%
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-4xl">
{pricingTiers.map((tier) => (
<div
key={tier.name}
className={cn(
"flex flex-col border p-8 space-y-6 relative",
tier.highlighted ? "border-2 border-personality shadow-lg scale-105" : "border-border",
)}
>
{tier.highlighted && (
<div className="absolute -top-4 left-4 bg-personality text-background px-3 py-1 text-xs font-700">
{tier.tagline}
</div>
)}
<div className="space-y-4">
<h3 className="text-3xl font-basteleur font-700">{tier.name}</h3>
<div className="flex items-baseline gap-2">
<span className="text-4xl font-700">
{billingPeriod === "annual" ? tier.priceAnnual : tier.price}
</span>
<span className="text-sm text-muted-foreground">
{billingPeriod === "annual" ? tier.periodAnnual : tier.period}
</span>
</div>
<p className="text-muted-foreground">{tier.description}</p>
</div>
<ul className="space-y-3 flex-1">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-sm">
<Icon
icon="check"
iconStyle={"pixel"}
className="size-6 -mt-0.5"
color="var(--personality)"
/>
<span>{feature}</span>
</li>
))}
</ul>
<Button
asChild
variant={tier.highlighted ? "default" : "outline"}
className={cn(
"font-700 py-6",
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
)}
>
<Link to={tier.ctaLink}>{tier.cta}</Link>
</Button>
</div>
))}
</div>
{/* trust signals */}
<div className="grid md:grid-cols-3 gap-8 w-full border-t pt-16 pb-8">
<div className="flex flex-col items-center text-center gap-2">
<Icon icon="eyeClosed" iconStyle={"pixel"} className="size-8" color="var(--personality)" />
<p className="font-700">Secure & Encrypted</p>
<p className="text-sm text-muted-foreground">Your data is safe with us</p>
</div>
<div className="flex flex-col items-center text-center gap-2">
<Icon
icon="creditCardDelete"
iconStyle={"pixel"}
className="size-8"
color="var(--personality)"
/>
<p className="font-700">No Card Required</p>
<p className="text-sm text-muted-foreground">Start your trial instantly</p>
</div>
<div className="flex flex-col items-center text-center gap-2">
<Icon icon="rotateCcw" iconStyle={"pixel"} className="size-8" color="var(--personality)" />
<p className="font-700">Money Back Guarantee</p>
<p className="text-sm text-muted-foreground">30-day no-risk policy</p>
</div>
</div>
{/* faq section */}
<div className="w-full max-w-5xl flex justify-center border-t pt-24 scroll-mt-4" id="faq">
<div className="w-full max-w-4xl flex flex-col items-center space-y-12">
<h2 className="text-5xl font-basteleur font-700 text-center">Frequently Asked Questions</h2>
<div className="grid gap-8 max-w-3xl">
{faqs.map((faq) => (
<div key={faq.question} className="space-y-2">
<h4 className="text-lg font-700">{faq.question}</h4>
<p className="text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
</div>
</div>
</div>
{/* TODO:> commented out until we have actual testimonies */}
{/* social proof placeholder */}
{/* <div className="max-w-4xl mx-auto text-center space-y-8">
<h2 className="text-4xl font-basteleur font-700">Join developers who've escaped Jira</h2>
<div className="grid md:grid-cols-3 gap-8">
<div className="border p-6 space-y-4">
<p className="text-lg italic text-muted-foreground">
"Finally, a project management tool that doesn't slow me down"
</p>
<p className="text-sm font-700">Early user feedback</p>
</div>
<div className="border p-6 space-y-4">
<p className="text-lg italic text-muted-foreground">
"Built by someone who actually understands developer workflows"
</p>
<p className="text-sm font-700">Early user feedback</p>
</div>
<div className="border p-6 space-y-4">
<p className="text-lg italic text-muted-foreground">
"The simplicity is refreshing. No bloat, just what we need"
</p>
<p className="text-sm font-700">Early user feedback</p>
</div>
</div>
</div> */}
{/* final cta */}
<div className="max-w-5xl mx-auto text-center space-y-6 border-t pt-16">
<h2 className="text-5xl font-basteleur font-700">Ready to ship faster?</h2>
<p className="text-xl text-muted-foreground">
Start tracking issues, managing sprints, and shipping products in minutes
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
{!isLoading && user ? (
<Button asChild size="lg" className="text-lg px-8 py-6">
<Link to="/issues">Open app</Link>
</Button>
) : (
<Button asChild size="lg" className="text-lg px-8 py-6">
<Link to="/login">Start your free trial</Link>
</Button>
)}
<div className="inline-flex gap-2 items-center">
<span className="relative">
<a
href="https://github.com/hex248/issue"
target="_blank"
rel="noopener noreferrer"
className="inline-flex gap-2 text-muted-foreground hover:text-personality"
>
<Icon icon="mdi:github" className="h-7 w-7" />
<span className="font-goudy font-700 text-2xl">GitHub</span>
</a>
<span className="text-violet-400/90 absolute left-full top-[45%] ml-4 -translate-y-1/2 whitespace-nowrap select-none text-muted-foreground">
{"<-- you can self-host me!"}
</span>
</span>
</div>
<p className="text-sm text-muted-foreground">
No credit card required · 14-day free trial · Cancel anytime
</p>
</div>
</div>
</main>
<footer className="flex justify-center gap-2 items-center py-1 border-t">
<span className="font-300 text-lg text-muted-foreground font-goudy">
<span className="font-300 text-lg text-muted-foreground">
Built by{" "}
<a
href="https://ob248.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-personality font-goudy font-700"
className="hover:text-personality font-700"
>
Oliver Bryan
</a>

View File

@@ -1,9 +1,10 @@
import Icon from "@/components/ui/icon";
import { Downasaur } from "@nsmr/pixelart-react";
export default function NotFound() {
return (
<div className={`w-full h-[100vh] flex flex-col items-center justify-center gap-4`}>
<Icon icon="circleQuestionMark" size={72} />
<span className="-ml-14 -mb-7 -rotate-20 text-xl text-muted-foreground">?</span>
<Downasaur size={72} color={"var(--personality)"} />
<span className="text-7xl font-500">404</span>
<span className="text-2xl font-400">Not Found</span>
</div>

View File

@@ -2,8 +2,7 @@ import Icon, { iconNames, iconStyles } from "@/components/ui/icon";
function Test() {
return (
<main className="w-full min-h-[100vh] flex flex-col justify-center items-center p-4">
<div>
<main className="w-full min-h-[100vh] flex flex-col justify-center items-center text-center p-4">
<div className="overflow-x-auto">
<table className="border-collapse">
<thead>
@@ -16,10 +15,7 @@ function Test() {
Name
</th>,
...iconStyles.map((iconStyle) => (
<th
key={`${iconStyle}-${groupNumber}`}
className="text-center p-2 font-medium capitalize"
>
<th key={`${iconStyle}-${groupNumber}`} className="text-center p-2 font-medium capitalize">
{iconStyle}
</th>
)),
@@ -52,7 +48,6 @@ function Test() {
</tbody>
</table>
</div>
</div>
</main>
);
}

14
todo.md
View File

@@ -1,15 +1,18 @@
# HIGH PRIORITY
- BUGS:
- org slug and project code should only be added to url on issues/timeline pages. it happens on any page right now
- on the first attempt since page load, pressing the create issue button the default type and status are not loaded
- FEATURES:
- pricing page
- see jira and other competitors
- explore payment providers (stripe is the only one i know)
- real logo
- make login/register into a modal that appears atop the landing page
- user preferences
- make pixel the default icon scheme
# LOW PRIORITY
- dedicated /register route (currently login/register are combined on /login)
- organisation
- see members' time tracking numbers
- export times to csv, json, etc.
- issues
- assignee "note" for extra context on their role in the task
- deadline
@@ -26,3 +29,4 @@
- open git diff in a new tab
- figure out if it's possible to remove the "lib/server/..." helpers altogether, and have some sort of dynamic route maker in the shared package
- request logging
- explore payment providers (stripe is the only one i know)