mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
Merge pull request #3 from hex248/development
Marketing ready landing page
This commit is contained in:
@@ -1,3 +1 @@
|
|||||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/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>
|
||||||
<circle cx="256" cy="256" r="192" stroke="#FFFFFF" stroke-width="32" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 298 B |
@@ -114,7 +114,7 @@ export function ServerConfiguration({ trigger }: { trigger?: ReactNode }) {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{trigger || (
|
{trigger || (
|
||||||
<IconButton size="lg" className="absolute top-2 right-2" title={"Server Configuration"}>
|
<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>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Alert as PixelAlert,
|
Alert as PixelAlert,
|
||||||
Check as PixelCheck,
|
Check as PixelCheck,
|
||||||
Checkbox as PixelCheckbox,
|
Checkbox as PixelCheckbox,
|
||||||
|
CheckboxOn as PixelCheckboxOn,
|
||||||
ChevronDown as PixelChevronDown,
|
ChevronDown as PixelChevronDown,
|
||||||
ChevronLeft as PixelChevronLeft,
|
ChevronLeft as PixelChevronLeft,
|
||||||
ChevronRight as PixelChevronRight,
|
ChevronRight as PixelChevronRight,
|
||||||
@@ -10,8 +11,14 @@ import {
|
|||||||
Clock as PixelClock,
|
Clock as PixelClock,
|
||||||
Close as PixelClose,
|
Close as PixelClose,
|
||||||
MessagePlus as PixelCommentSend,
|
MessagePlus as PixelCommentSend,
|
||||||
|
CreditCard as PixelCreditCard,
|
||||||
|
CreditCardDelete as PixelCreditCardDelete,
|
||||||
|
Dashboard as PixelDashboard,
|
||||||
Debug as PixelDebug,
|
Debug as PixelDebug,
|
||||||
|
DebugOff as PixelDebugOff,
|
||||||
Edit as PixelEdit,
|
Edit as PixelEdit,
|
||||||
|
EyeClosed as PixelEyeClosed,
|
||||||
|
AddGrid as PixelGridAdd,
|
||||||
Home as PixelHome,
|
Home as PixelHome,
|
||||||
InfoBox as PixelInfo,
|
InfoBox as PixelInfo,
|
||||||
Link as PixelLink,
|
Link as PixelLink,
|
||||||
@@ -24,14 +31,18 @@ import {
|
|||||||
Play as PixelPlay,
|
Play as PixelPlay,
|
||||||
Plus as PixelPlus,
|
Plus as PixelPlus,
|
||||||
Server as PixelServer,
|
Server as PixelServer,
|
||||||
|
Shield as PixelShield,
|
||||||
|
Ship as PixelShip,
|
||||||
CheckboxOn as PixelStop,
|
CheckboxOn as PixelStop,
|
||||||
Sun as PixelSun,
|
Sun as PixelSun,
|
||||||
Trash as PixelTrash,
|
Trash as PixelTrash,
|
||||||
Undo as PixelUndo,
|
Undo as PixelUndo,
|
||||||
User as PixelUser,
|
User as PixelUser,
|
||||||
ViewportWide as PixelViewportWide,
|
ViewportWide as PixelViewportWide,
|
||||||
|
Zap as PixelZap,
|
||||||
} from "@nsmr/pixelart-react";
|
} from "@nsmr/pixelart-react";
|
||||||
import {
|
import {
|
||||||
|
ArrowCounterClockwiseIcon as PhosphorArrowCounterClockwise,
|
||||||
BugIcon as PhosphorBug,
|
BugIcon as PhosphorBug,
|
||||||
CheckIcon as PhosphorCheck,
|
CheckIcon as PhosphorCheck,
|
||||||
CheckCircleIcon as PhosphorCheckCircle,
|
CheckCircleIcon as PhosphorCheckCircle,
|
||||||
@@ -41,15 +52,19 @@ import {
|
|||||||
CaretRightIcon as PhosphorChevronRight,
|
CaretRightIcon as PhosphorChevronRight,
|
||||||
CaretUpIcon as PhosphorChevronUp,
|
CaretUpIcon as PhosphorChevronUp,
|
||||||
CircleIcon as PhosphorCircle,
|
CircleIcon as PhosphorCircle,
|
||||||
ClockIcon as PhosphorClock,
|
|
||||||
ChatTextIcon as PhosphorComment,
|
ChatTextIcon as PhosphorComment,
|
||||||
|
CreditCardIcon as PhosphorCreditCard,
|
||||||
|
CubeIcon as PhosphorCube,
|
||||||
DotsSixVerticalIcon as PhosphorDotsSixVertical,
|
DotsSixVerticalIcon as PhosphorDotsSixVertical,
|
||||||
DotsThreeVerticalIcon as PhosphorDotsThreeVertical,
|
DotsThreeVerticalIcon as PhosphorDotsThreeVertical,
|
||||||
PencilSimpleIcon as PhosphorEdit,
|
PencilSimpleIcon as PhosphorEdit,
|
||||||
|
EyeClosedIcon as PhosphorEyeClosed,
|
||||||
HashIcon as PhosphorHash,
|
HashIcon as PhosphorHash,
|
||||||
HashStraightIcon as PhosphorHashStraight,
|
HashStraightIcon as PhosphorHashStraight,
|
||||||
HouseIcon as PhosphorHome,
|
HouseIcon as PhosphorHome,
|
||||||
InfoIcon as PhosphorInfo,
|
InfoIcon as PhosphorInfo,
|
||||||
|
LayoutIcon as PhosphorLayout,
|
||||||
|
LightningIcon as PhosphorLightning,
|
||||||
LinkIcon as PhosphorLink,
|
LinkIcon as PhosphorLink,
|
||||||
SpinnerGapIcon as PhosphorLoader,
|
SpinnerGapIcon as PhosphorLoader,
|
||||||
SignOutIcon as PhosphorLogOut,
|
SignOutIcon as PhosphorLogOut,
|
||||||
@@ -59,11 +74,14 @@ import {
|
|||||||
PlayIcon as PhosphorPlay,
|
PlayIcon as PhosphorPlay,
|
||||||
PlusIcon as PhosphorPlus,
|
PlusIcon as PhosphorPlus,
|
||||||
QuestionIcon as PhosphorQuestion,
|
QuestionIcon as PhosphorQuestion,
|
||||||
|
RocketLaunchIcon as PhosphorRocketLaunch,
|
||||||
HardDrivesIcon as PhosphorServer,
|
HardDrivesIcon as PhosphorServer,
|
||||||
|
ShieldCheckIcon as PhosphorShieldCheck,
|
||||||
|
StackPlusIcon as PhosphorStackPlus,
|
||||||
StopIcon as PhosphorStop,
|
StopIcon as PhosphorStop,
|
||||||
SunIcon as PhosphorSun,
|
SunIcon as PhosphorSun,
|
||||||
|
TimerIcon as PhosphorTimer,
|
||||||
TrashIcon as PhosphorTrash,
|
TrashIcon as PhosphorTrash,
|
||||||
ArrowCounterClockwiseIcon as PhosphorUndo,
|
|
||||||
UserIcon as PhosphorUser,
|
UserIcon as PhosphorUser,
|
||||||
WarningIcon as PhosphorWarning,
|
WarningIcon as PhosphorWarning,
|
||||||
XIcon as PhosphorX,
|
XIcon as PhosphorX,
|
||||||
@@ -71,7 +89,9 @@ import {
|
|||||||
import type { IconStyle } from "@sprint/shared";
|
import type { IconStyle } from "@sprint/shared";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Box,
|
||||||
Bug,
|
Bug,
|
||||||
|
BugOff,
|
||||||
Check,
|
Check,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -79,15 +99,18 @@ import {
|
|||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronUpIcon,
|
|
||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
CircleQuestionMark,
|
CircleQuestionMark,
|
||||||
|
CreditCard,
|
||||||
Edit,
|
Edit,
|
||||||
EllipsisVertical,
|
EllipsisVertical,
|
||||||
|
EyeClosed,
|
||||||
|
Grid2x2Plus as GridAdd,
|
||||||
GripVerticalIcon,
|
GripVerticalIcon,
|
||||||
Hash,
|
Hash,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
|
LayoutDashboard,
|
||||||
Link,
|
Link,
|
||||||
Loader,
|
Loader,
|
||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
@@ -99,17 +122,22 @@ import {
|
|||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
|
Rocket,
|
||||||
|
RotateCcw,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
ShieldCheck,
|
||||||
Square,
|
Square,
|
||||||
SquareCheck,
|
SquareCheck,
|
||||||
Sun,
|
Sun,
|
||||||
Timer,
|
Timer,
|
||||||
|
TimerOff,
|
||||||
Trash,
|
Trash,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
Undo,
|
Undo,
|
||||||
Undo2,
|
Undo2,
|
||||||
UserRound,
|
UserRound,
|
||||||
X,
|
X,
|
||||||
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useSessionSafe } from "@/components/session-provider";
|
import { useSessionSafe } from "@/components/session-provider";
|
||||||
|
|
||||||
@@ -118,7 +146,9 @@ import { useSessionSafe } from "@/components/session-provider";
|
|||||||
// phosphor: https://phosphoricons.com/
|
// phosphor: https://phosphoricons.com/
|
||||||
const icons = {
|
const icons = {
|
||||||
alertTriangle: { lucide: AlertTriangle, pixel: PixelAlert, phosphor: PhosphorWarning },
|
alertTriangle: { lucide: AlertTriangle, pixel: PixelAlert, phosphor: PhosphorWarning },
|
||||||
|
box: { lucide: Box, pixel: PixelCheckboxOn, phosphor: PhosphorCube },
|
||||||
bug: { lucide: Bug, pixel: PixelDebug, phosphor: PhosphorBug },
|
bug: { lucide: Bug, pixel: PixelDebug, phosphor: PhosphorBug },
|
||||||
|
bugOff: { lucide: BugOff, pixel: PixelDebugOff, phosphor: PhosphorBug },
|
||||||
check: { lucide: Check, pixel: PixelCheck, phosphor: PhosphorCheck },
|
check: { lucide: Check, pixel: PixelCheck, phosphor: PhosphorCheck },
|
||||||
checkIcon: { lucide: CheckIcon, pixel: PixelCheck, phosphor: PhosphorCheck },
|
checkIcon: { lucide: CheckIcon, pixel: PixelCheck, phosphor: PhosphorCheck },
|
||||||
checkBox: { lucide: SquareCheck, pixel: PixelCheckbox, phosphor: PhosphorCheckSquare },
|
checkBox: { lucide: SquareCheck, pixel: PixelCheckbox, phosphor: PhosphorCheckSquare },
|
||||||
@@ -127,17 +157,20 @@ const icons = {
|
|||||||
chevronLeftIcon: { lucide: ChevronLeftIcon, pixel: PixelChevronLeft, phosphor: PhosphorChevronLeft },
|
chevronLeftIcon: { lucide: ChevronLeftIcon, pixel: PixelChevronLeft, phosphor: PhosphorChevronLeft },
|
||||||
chevronRightIcon: { lucide: ChevronRightIcon, pixel: PixelChevronRight, phosphor: PhosphorChevronRight },
|
chevronRightIcon: { lucide: ChevronRightIcon, pixel: PixelChevronRight, phosphor: PhosphorChevronRight },
|
||||||
chevronUp: { lucide: ChevronUp, pixel: PixelChevronUp, phosphor: PhosphorChevronUp },
|
chevronUp: { lucide: ChevronUp, pixel: PixelChevronUp, phosphor: PhosphorChevronUp },
|
||||||
chevronUpIcon: { lucide: ChevronUpIcon, pixel: PixelChevronUp, phosphor: PhosphorChevronUp },
|
circleCheck: { lucide: CircleCheckIcon, pixel: PixelCheck, phosphor: PhosphorCheckCircle },
|
||||||
circleCheckIcon: { lucide: CircleCheckIcon, pixel: PixelCheck, phosphor: PhosphorCheckCircle },
|
|
||||||
circleIcon: { lucide: CircleIcon, pixel: PixelCircle, phosphor: PhosphorCircle },
|
circleIcon: { lucide: CircleIcon, pixel: PixelCircle, phosphor: PhosphorCircle },
|
||||||
circleQuestionMark: { lucide: CircleQuestionMark, pixel: PixelNoteDelete, phosphor: PhosphorQuestion },
|
circleQuestionMark: { lucide: CircleQuestionMark, pixel: PixelNoteDelete, phosphor: PhosphorQuestion },
|
||||||
comment: { lucide: MessageSquarePlus, pixel: PixelCommentSend, phosphor: PhosphorComment },
|
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 },
|
edit: { lucide: Edit, pixel: PixelEdit, phosphor: PhosphorEdit },
|
||||||
ellipsisVertical: {
|
ellipsisVertical: {
|
||||||
lucide: EllipsisVertical,
|
lucide: EllipsisVertical,
|
||||||
pixel: PixelMoreVertical,
|
pixel: PixelMoreVertical,
|
||||||
phosphor: PhosphorDotsThreeVertical,
|
phosphor: PhosphorDotsThreeVertical,
|
||||||
},
|
},
|
||||||
|
eyeClosed: { lucide: EyeClosed, pixel: PixelEyeClosed, phosphor: PhosphorEyeClosed },
|
||||||
|
gridAdd: { lucide: GridAdd, pixel: PixelGridAdd, phosphor: PhosphorStackPlus },
|
||||||
gripVerticalIcon: {
|
gripVerticalIcon: {
|
||||||
lucide: GripVerticalIcon,
|
lucide: GripVerticalIcon,
|
||||||
pixel: PixelViewportWide,
|
pixel: PixelViewportWide,
|
||||||
@@ -145,26 +178,32 @@ const icons = {
|
|||||||
},
|
},
|
||||||
hash: { lucide: Hash, pixel: PhosphorHashStraight, phosphor: PhosphorHash },
|
hash: { lucide: Hash, pixel: PhosphorHashStraight, phosphor: PhosphorHash },
|
||||||
home: { lucide: LucideHome, pixel: PixelHome, phosphor: PhosphorHome },
|
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 },
|
link: { lucide: Link, pixel: PixelLink, phosphor: PhosphorLink },
|
||||||
loader: { lucide: Loader, pixel: PixelLoader, phosphor: PhosphorLoader },
|
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 },
|
logOut: { lucide: LogOut, pixel: PixelLogout, phosphor: PhosphorLogOut },
|
||||||
moon: { lucide: Moon, pixel: PixelMoon, phosphor: PhosphorMoon },
|
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 },
|
pause: { lucide: Pause, pixel: PixelPause, phosphor: PhosphorPause },
|
||||||
play: { lucide: Play, pixel: PixelPlay, phosphor: PhosphorPlay },
|
play: { lucide: Play, pixel: PixelPlay, phosphor: PhosphorPlay },
|
||||||
plus: { lucide: Plus, pixel: PixelPlus, phosphor: PhosphorPlus },
|
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 },
|
sun: { lucide: Sun, pixel: PixelSun, phosphor: PhosphorSun },
|
||||||
stop: { lucide: Square, pixel: PixelStop, phosphor: PhosphorStop },
|
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 },
|
trash: { lucide: Trash, pixel: PixelTrash, phosphor: PhosphorTrash },
|
||||||
triangleAlertIcon: { lucide: TriangleAlertIcon, pixel: PixelAlert, phosphor: PhosphorWarning },
|
triangleAlert: { lucide: TriangleAlertIcon, pixel: PixelAlert, phosphor: PhosphorWarning },
|
||||||
undo: { lucide: Undo, pixel: PixelUndo, phosphor: PhosphorUndo },
|
undo: { lucide: Undo, pixel: PixelUndo, phosphor: PhosphorArrowCounterClockwise },
|
||||||
undo2: { lucide: Undo2, pixel: PixelUndo, phosphor: PhosphorUndo },
|
undo2: { lucide: Undo2, pixel: PixelUndo, phosphor: PhosphorArrowCounterClockwise },
|
||||||
userRound: { lucide: UserRound, pixel: PixelUser, phosphor: PhosphorUser },
|
userRound: { lucide: UserRound, pixel: PixelUser, phosphor: PhosphorUser },
|
||||||
x: { lucide: X, pixel: PixelClose, phosphor: PhosphorX },
|
x: { lucide: X, pixel: PixelClose, phosphor: PhosphorX },
|
||||||
|
zap: { lucide: Zap, pixel: PixelZap, phosphor: PhosphorLightning },
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconName = keyof typeof icons;
|
export type IconName = keyof typeof icons;
|
||||||
@@ -202,7 +241,7 @@ export default function Icon({
|
|||||||
// lucide fills sillily
|
// lucide fills sillily
|
||||||
if (color && resolvedStyle !== "lucide") {
|
if (color && resolvedStyle !== "lucide") {
|
||||||
fill = color;
|
fill = color;
|
||||||
} else if (resolvedStyle === "pixel" && ["bug", "moon", "hash"].includes(icon)) {
|
} else if (resolvedStyle === "pixel" && ["bug", "moon", "hash", "bugOff"].includes(icon)) {
|
||||||
fill = "var(--foreground)";
|
fill = "var(--foreground)";
|
||||||
} else if (resolvedStyle === "phosphor") {
|
} else if (resolvedStyle === "phosphor") {
|
||||||
fill = "var(--foreground)";
|
fill = "var(--foreground)";
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ function SelectScrollUpButton({
|
|||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon icon="chevronUpIcon" className="size-4" />
|
<Icon icon="chevronUp" className="size-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
icons={{
|
icons={{
|
||||||
success: <Icon icon="circleCheckIcon" className="size-4" />,
|
success: <Icon icon="circleCheck" className="size-4" />,
|
||||||
info: <Icon icon="infoIcon" className="size-4" />,
|
info: <Icon icon="info" className="size-4" />,
|
||||||
warning: <Icon icon="triangleAlertIcon" className="size-4" />,
|
warning: <Icon icon="triangleAlert" className="size-4" />,
|
||||||
error: <Icon icon="octagonXIcon" className="size-4" />,
|
error: <Icon icon="octagonX" className="size-4" />,
|
||||||
loading: <Icon icon="loader2Icon" className="size-4 animate-spin" />,
|
loading: <Icon icon="loader2" className="size-4 animate-spin" />,
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
function Switch({
|
function Switch({
|
||||||
className,
|
className,
|
||||||
|
thumbClassName,
|
||||||
size = "default",
|
size = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||||
size?: "sm" | "default";
|
thumbClassName?: string;
|
||||||
|
size?: "sm" | "default" | "lg";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SwitchPrimitive.Root
|
<SwitchPrimitive.Root
|
||||||
@@ -18,8 +20,8 @@ function Switch({
|
|||||||
"peer data-[state=checked]:bg-personality data-[state=unchecked]:bg-input",
|
"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",
|
"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",
|
"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",
|
"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=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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -27,7 +29,8 @@ function Switch({
|
|||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
data-slot="switch-thumb"
|
data-slot="switch-thumb"
|
||||||
className={cn(
|
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>
|
</SwitchPrimitive.Root>
|
||||||
|
|||||||
@@ -1,83 +1,497 @@
|
|||||||
import { Icon } from "@iconify/react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useSession } from "@/components/session-provider";
|
import { useSession } from "@/components/session-provider";
|
||||||
import ThemeToggle from "@/components/theme-toggle";
|
import ThemeToggle from "@/components/theme-toggle";
|
||||||
import { Button } from "@/components/ui/button";
|
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() {
|
export default function Landing() {
|
||||||
const { user, isLoading } = useSession();
|
const { user, isLoading } = useSession();
|
||||||
|
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col" id="top">
|
||||||
<header className="relative flex items-center justify-center p-2 border-b">
|
<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-md">
|
||||||
<div className="text-3xl font-basteleur font-700">Sprint</div>
|
<div className="w-full flex h-14 items-center justify-between px-2">
|
||||||
<nav className="absolute right-2 flex items-center gap-4">
|
<div className="flex items-center gap-2">
|
||||||
<ThemeToggle />
|
<img src="/favicon.svg" alt="Sprint" className="size-12 -mt-0.5" />
|
||||||
{!isLoading && user ? (
|
<span className="text-3xl font-basteleur font-700 transition-colors -mt-0.5">Sprint</span>
|
||||||
<>
|
</div>
|
||||||
{user && (
|
<nav className="flex items-center gap-6">
|
||||||
<h1 className="text-xl font-basteleur font-400">Welcome back {user.name.split(" ")[0]}!</h1>
|
<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 ? (
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
<Button asChild variant="outline" size="sm">
|
</div>
|
||||||
<Link to="/issues">Open app</Link>
|
</nav>
|
||||||
</Button>
|
</div>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button asChild variant="outline" size="sm">
|
|
||||||
<Link to="/login">Sign in</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 flex flex-col items-center justify-center gap-8">
|
<main className="flex-1 flex flex-col items-center py-16 pt-14 px-4">
|
||||||
<div className="max-w-3xl text-center space-y-4">
|
<div className="max-w-6xl w-full space-y-18">
|
||||||
<h1 className="text-[54px] font-basteleur font-700">Need a snappy project management tool?</h1>
|
{/* hero section */}
|
||||||
<p className="text-[24px] font-goudy text-muted-foreground">
|
<div className="text-center space-y-8">
|
||||||
Build your next project with <span className="font-goudy font-700">Sprint.</span>
|
<div className="flex justify-center mb-8">
|
||||||
</p>
|
<img src="/favicon.svg" alt="Sprint" className="size-48" />
|
||||||
<p className="text-[18px] font-goudy text-muted-foreground font-700">
|
</div>
|
||||||
Sick of Jira? Say hello to your new favorite project management tool.
|
<div className="space-y-4">
|
||||||
</p>
|
<h1 className="text-[64px] font-basteleur font-700 leading-tight">
|
||||||
</div>
|
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 moving—without 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 ? (
|
{!isLoading && user ? (
|
||||||
<Button asChild size="lg">
|
<Button asChild size="lg" className="text-lg px-8 py-6">
|
||||||
<Link to="/issues">Open app</Link>
|
<Link to="/issues">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild size="lg">
|
<>
|
||||||
<Link to="/login">Get started</Link>
|
<Button asChild size="lg" className="text-lg px-8 py-6">
|
||||||
</Button>
|
<Link to="/login">Start free trial</Link>
|
||||||
)}
|
</Button>
|
||||||
<div className="inline-flex gap-2 items-center">
|
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
|
||||||
<span className="relative">
|
<a href="#pricing">See pricing</a>
|
||||||
<a
|
</Button>
|
||||||
href="https://github.com/hex248/issue"
|
</>
|
||||||
target="_blank"
|
)}
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
className="inline-flex gap-2 text-muted-foreground hover:text-personality"
|
|
||||||
>
|
<p className="text-sm text-muted-foreground">No credit card required · Full access for 14 days</p>
|
||||||
<Icon icon="mdi:github" className="h-7 w-7" />
|
</div>
|
||||||
<span className="font-goudy font-700 text-2xl">GitHub</span>
|
|
||||||
</a>
|
{/* problem section */}
|
||||||
<span className="text-violet-400/90 absolute left-full top-[45%] ml-4 -translate-y-1/2 whitespace-nowrap select-none text-muted-foreground">
|
<div className="max-w-4xl mx-auto space-y-16">
|
||||||
{"<-- you can self-host me!"}
|
<h2 className="text-4xl font-basteleur font-700 text-center">
|
||||||
</span>
|
Tired of spending more time managing Jira than building products?
|
||||||
</span>
|
</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>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No credit card required · 14-day free trial · Cancel anytime
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="flex justify-center gap-2 items-center py-1 border-t">
|
<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{" "}
|
Built by{" "}
|
||||||
<a
|
<a
|
||||||
href="https://ob248.com"
|
href="https://ob248.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hover:text-personality font-goudy font-700"
|
className="hover:text-personality font-700"
|
||||||
>
|
>
|
||||||
Oliver Bryan
|
Oliver Bryan
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Icon from "@/components/ui/icon";
|
import { Downasaur } from "@nsmr/pixelart-react";
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className={`w-full h-[100vh] flex flex-col items-center justify-center gap-4`}>
|
<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-7xl font-500">404</span>
|
||||||
<span className="text-2xl font-400">Not Found</span>
|
<span className="text-2xl font-400">Not Found</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,56 +2,51 @@ import Icon, { iconNames, iconStyles } from "@/components/ui/icon";
|
|||||||
|
|
||||||
function Test() {
|
function Test() {
|
||||||
return (
|
return (
|
||||||
<main className="w-full min-h-[100vh] flex flex-col justify-center items-center p-4">
|
<main className="w-full min-h-[100vh] flex flex-col justify-center items-center text-center p-4">
|
||||||
<div>
|
<div className="overflow-x-auto">
|
||||||
<div className="overflow-x-auto">
|
<table className="border-collapse">
|
||||||
<table className="border-collapse">
|
<thead>
|
||||||
<thead>
|
<tr className="border-b">
|
||||||
<tr className="border-b">
|
{Array.from(
|
||||||
|
{ length: Math.ceil(iconNames.length / 10) },
|
||||||
|
(_, groupNumber) => groupNumber + 1,
|
||||||
|
).flatMap((groupNumber) => [
|
||||||
|
<th key={`name-${groupNumber}`} className="text-left p-2 font-medium">
|
||||||
|
Name
|
||||||
|
</th>,
|
||||||
|
...iconStyles.map((iconStyle) => (
|
||||||
|
<th key={`${iconStyle}-${groupNumber}`} className="text-center p-2 font-medium capitalize">
|
||||||
|
{iconStyle}
|
||||||
|
</th>
|
||||||
|
)),
|
||||||
|
])}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 10 }, (_, rowNumber) => rowNumber + 1).map((rowNumber) => (
|
||||||
|
<tr key={`row-${rowNumber}`} className="border-b hover:bg-muted/50">
|
||||||
{Array.from(
|
{Array.from(
|
||||||
{ length: Math.ceil(iconNames.length / 10) },
|
{ length: Math.ceil(iconNames.length / 10) },
|
||||||
(_, groupNumber) => groupNumber + 1,
|
(_, groupNumber) => groupNumber + 1,
|
||||||
).flatMap((groupNumber) => [
|
).flatMap((groupNumber) => {
|
||||||
<th key={`name-${groupNumber}`} className="text-left p-2 font-medium">
|
const iconIndex = (groupNumber - 1) * 10 + (rowNumber - 1);
|
||||||
Name
|
const name = iconNames[iconIndex];
|
||||||
</th>,
|
|
||||||
...iconStyles.map((iconStyle) => (
|
|
||||||
<th
|
|
||||||
key={`${iconStyle}-${groupNumber}`}
|
|
||||||
className="text-center p-2 font-medium capitalize"
|
|
||||||
>
|
|
||||||
{iconStyle}
|
|
||||||
</th>
|
|
||||||
)),
|
|
||||||
])}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 10 }, (_, rowNumber) => rowNumber + 1).map((rowNumber) => (
|
|
||||||
<tr key={`row-${rowNumber}`} className="border-b hover:bg-muted/50">
|
|
||||||
{Array.from(
|
|
||||||
{ length: Math.ceil(iconNames.length / 10) },
|
|
||||||
(_, groupNumber) => groupNumber + 1,
|
|
||||||
).flatMap((groupNumber) => {
|
|
||||||
const iconIndex = (groupNumber - 1) * 10 + (rowNumber - 1);
|
|
||||||
const name = iconNames[iconIndex];
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<td key={`name-${groupNumber}-${rowNumber}`} className="font-mono text-sm pl-2 pr-12">
|
<td key={`name-${groupNumber}-${rowNumber}`} className="font-mono text-sm pl-2 pr-12">
|
||||||
{name ?? ""}
|
{name ?? ""}
|
||||||
</td>,
|
</td>,
|
||||||
...iconStyles.map((iconStyle) => (
|
...iconStyles.map((iconStyle) => (
|
||||||
<td key={`${iconStyle}-${groupNumber}-${rowNumber}`} className="p-2 text-center">
|
<td key={`${iconStyle}-${groupNumber}-${rowNumber}`} className="p-2 text-center">
|
||||||
{name ? <Icon icon={name} iconStyle={iconStyle} size={24} /> : null}
|
{name ? <Icon icon={name} iconStyle={iconStyle} size={24} /> : null}
|
||||||
</td>
|
</td>
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
14
todo.md
14
todo.md
@@ -1,15 +1,18 @@
|
|||||||
# HIGH PRIORITY
|
# HIGH PRIORITY
|
||||||
|
|
||||||
- BUGS:
|
- 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:
|
- FEATURES:
|
||||||
- pricing page
|
- make login/register into a modal that appears atop the landing page
|
||||||
- see jira and other competitors
|
- user preferences
|
||||||
- explore payment providers (stripe is the only one i know)
|
- make pixel the default icon scheme
|
||||||
- real logo
|
|
||||||
|
|
||||||
# LOW PRIORITY
|
# 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
|
- issues
|
||||||
- assignee "note" for extra context on their role in the task
|
- assignee "note" for extra context on their role in the task
|
||||||
- deadline
|
- deadline
|
||||||
@@ -26,3 +29,4 @@
|
|||||||
- open git diff in a new tab
|
- 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
|
- 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
|
- request logging
|
||||||
|
- explore payment providers (stripe is the only one i know)
|
||||||
|
|||||||
Reference in New Issue
Block a user