feat: features breakdown

This commit is contained in:
Henry
2024-12-02 21:21:15 +00:00
parent 24702f5e86
commit be184efee3
21 changed files with 63720 additions and 43 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3490
src/assets/imports-dark.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1675
src/assets/labels-dark.json Normal file

File diff suppressed because it is too large Load Diff

1675
src/assets/labels-light.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import chatIconLight from "~/assets/chat-light.json";
import chatIconDark from "~/assets/chat-dark.json";
const FeedbackButton: React.FC = () => {
const { theme } = useTheme();
const { activeTheme } = useTheme();
const [isHovered, setIsHovered] = useState(false);
const [index, setIndex] = useState(0);
@@ -23,7 +23,7 @@ const FeedbackButton: React.FC = () => {
>
<LottieIcon
index={index}
json={theme === "dark" ? chatIconDark : chatIconLight}
json={activeTheme === "dark" ? chatIconDark : chatIconLight}
isPlaying={isHovered}
/>
<span className="ml-1">Feedback</span>

View File

@@ -28,23 +28,25 @@ export default function SideNavigation({
isLoading,
}: SideNavigationProps) {
const pathname = usePathname();
const { theme } = useTheme();
const { activeTheme } = useTheme();
const isDarkMode = activeTheme === "dark";
const navigation = [
{
name: "Boards",
href: "/boards",
icon: theme === "dark" ? boardsIconDark : boardsIconLight,
icon: isDarkMode ? boardsIconDark : boardsIconLight,
},
{
name: "Members",
href: "/members",
icon: theme === "dark" ? membersIconDark : membersIconLight,
icon: isDarkMode ? membersIconDark : membersIconLight,
},
{
name: "Settings",
href: "/settings",
icon: theme === "dark" ? settingsIconDark : settingsIconLight,
icon: isDarkMode ? settingsIconDark : settingsIconLight,
},
];

View File

@@ -20,7 +20,7 @@ export default function UserMenu({
isLoading,
}: UserMenuProps) {
const router = useRouter();
const { theme, switchTheme } = useTheme();
const { themePreference, switchTheme } = useTheme();
const handleLogout = async () => {
const db = createClient();
@@ -86,7 +86,7 @@ export default function UserMenu({
>
<span
className={classNames(
theme === "system" ? "visible" : "invisible",
themePreference === "system" ? "visible" : "invisible",
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
)}
/>
@@ -100,7 +100,7 @@ export default function UserMenu({
>
<span
className={classNames(
theme === "dark" ? "visible" : "invisible",
themePreference === "dark" ? "visible" : "invisible",
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
)}
/>
@@ -114,7 +114,7 @@ export default function UserMenu({
>
<span
className={classNames(
theme === "light" ? "visible" : "invisible",
themePreference === "light" ? "visible" : "invisible",
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
)}
/>

View File

@@ -7,8 +7,9 @@ import React, {
} from "react";
interface ThemeContextProps {
theme: string;
switchTheme: (theme: string) => void;
themePreference: "light" | "dark" | "system";
activeTheme: "light" | "dark";
switchTheme: (theme: "light" | "dark" | "system") => void;
}
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
@@ -16,45 +17,45 @@ const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [theme, setTheme] = useState("");
const [themePreference, setThemePreference] = useState<
"light" | "dark" | "system"
>("system");
const [activeTheme, setActiveTheme] = useState<"light" | "dark">("light");
const switchTheme = (theme: string) => {
const switchTheme = (theme: "light" | "dark" | "system") => {
if (theme === "system") {
localStorage.removeItem("theme");
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setActiveTheme(isDark ? "dark" : "light");
document.documentElement.classList.toggle("dark", isDark);
} else {
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
localStorage.theme = theme;
setActiveTheme(isDark ? "dark" : "light");
}
setTheme(theme);
setThemePreference(theme);
};
useEffect(() => {
if (!("theme" in localStorage)) {
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
setTheme("system");
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", isDark);
setActiveTheme(isDark ? "dark" : "light");
setThemePreference("system");
} else {
if (localStorage.theme === "dark") {
document.documentElement.classList.add("dark");
setTheme("dark");
} else {
document.documentElement.classList.remove("dark");
setTheme("light");
}
const isDark = localStorage.theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
setActiveTheme(isDark ? "dark" : "light");
setThemePreference(localStorage.theme as "light" | "dark");
}
}, []);
return (
<ThemeContext.Provider value={{ switchTheme, theme }}>
{theme.length ? children : null}
<ThemeContext.Provider
value={{ switchTheme, themePreference, activeTheme }}
>
{themePreference.length ? children : null}
</ThemeContext.Provider>
);
};

View File

@@ -0,0 +1,155 @@
import { useState } from "react";
import Link from "next/link";
import LottieIcon from "~/components/LottieIcon";
import boardVisibilityIconLight from "~/assets/board-visibility-light.json";
import boardVisibilityIconDark from "~/assets/board-visibility-dark.json";
import membersIconLight from "~/assets/members-light.json";
import membersIconDark from "~/assets/members-dark.json";
import commentsIconLight from "~/assets/comments-light.json";
import commentsIconDark from "~/assets/comments-dark.json";
import integrationsIconLight from "~/assets/integrations-light.json";
import integrationsIconDark from "~/assets/integrations-dark.json";
import labelsIconLight from "~/assets/labels-light.json";
import labelsIconDark from "~/assets/labels-dark.json";
import importsIconLight from "~/assets/imports-light.json";
import importsIconDark from "~/assets/imports-dark.json";
import activityLogsIconLight from "~/assets/activity-logs-light.json";
import activityLogsIconDark from "~/assets/activity-logs-dark.json";
import templatesIconLight from "~/assets/templates-light.json";
import templatesIconDark from "~/assets/templates-dark.json";
const FeatureItem = ({
feature,
}: {
feature: {
title: string;
description: string;
icon: Record<string, unknown>;
comingSoon?: boolean;
};
}) => {
const [isHovered, setIsHovered] = useState(false);
const [index, setIndex] = useState(0);
const handleMouseEnter = () => {
setIsHovered(true);
setIndex((index) => index + 1);
};
return (
<div
onMouseEnter={handleMouseEnter}
className="group relative flex h-56 w-56 flex-col items-center justify-center overflow-hidden rounded-3xl border border-light-200 bg-light-50 dark:border-dark-200 dark:bg-dark-50"
>
<div className="absolute left-8 top-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200 " />
<div className="absolute right-8 top-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
<div className="absolute bottom-8 left-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
<div className="absolute bottom-8 right-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
<div className="flex h-10 w-10 items-center justify-center rounded-xl border border-light-300 bg-light-200 dark:border-dark-600 dark:bg-dark-200">
<LottieIcon index={index} json={feature.icon} isPlaying={isHovered} />
</div>
<div className="relative mt-2 w-full px-4 text-center">
<p className="text-sm font-bold text-light-1000 transition-opacity duration-200 group-hover:opacity-0 dark:text-dark-1000">
{feature.title}
</p>
<p className="absolute inset-0 px-4 text-sm text-light-950 opacity-0 transition-opacity duration-200 group-hover:opacity-100 dark:text-dark-900">
{feature.description}
</p>
</div>
{feature.comingSoon && (
<div className="absolute right-4 top-4 rounded-full border border-light-300 px-2 py-1 text-[10px] text-light-1000 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900">
Coming soon
</div>
)}
</div>
);
};
const Features = ({ theme }: { theme: "light" | "dark" }) => {
const isDark = theme === "dark";
const features = [
{
title: "Board visibility",
description: "Control who can view and edit your boards.",
icon: isDark ? boardVisibilityIconDark : boardVisibilityIconLight,
},
{
title: "Workspace members",
description: "Collaborate seamlessly with your team.",
icon: isDark ? membersIconDark : membersIconLight,
},
{
title: "Trello imports",
description: "Import your Trello boards and hit the ground running.",
icon: isDark ? importsIconDark : importsIconLight,
},
{
title: "Labels & Filters",
description:
"Organize and find cards quickly with powerful filtering tools.",
icon: isDark ? labelsIconDark : labelsIconLight,
},
{
title: "Comments",
description: "Discuss and collaborate on cards.",
icon: isDark ? commentsIconDark : commentsIconLight,
},
{
title: "Activity logs",
description: "Track all card changes with detailed activity history.",
icon: isDark ? activityLogsIconDark : activityLogsIconLight,
},
{
title: "Templates",
description: "Save time with reusable board templates.",
icon: isDark ? templatesIconDark : templatesIconLight,
comingSoon: true,
},
{
title: "Integrations",
description: "Connect your favorite tools to streamline your workflow.",
icon: isDark ? integrationsIconDark : integrationsIconLight,
comingSoon: true,
},
];
return (
<>
<div className="flex flex-col items-center justify-center pb-24">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<p>Features</p>
</div>
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000">
Kanban simplified
</p>
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
Simple, visual task management that just works. Drag and drop cards,
collaborate with your team, and get more done.
</p>
<div className="mt-16 grid grid-cols-4 gap-6 [mask-image:linear-gradient(to_bottom,black_80%,transparent_100%)]">
{features.map((feature, index) => {
return <FeatureItem key={`feature-${index}`} feature={feature} />;
})}
</div>
<div>
<div className="mt-8 flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<p>
{`We're just getting started. `}
<Link href="/roadmap" className="underline">
View our roadmap.
</Link>
</p>
</div>
</div>
</div>
</>
);
};
export default Features;

View File

@@ -64,7 +64,7 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
</div>
</div>
</div>
{isScrolled && <div className="h-[4rem] min-h-[4rem]"></div>}
{isScrolled && <div className="h-[5rem] min-h-[5rem]"></div>}
</>
);
};

View File

@@ -7,6 +7,7 @@ import Button from "~/components/Button";
import PatternedBackground from "~/components/PatternedBackground";
import Header from "./components/Header";
import Features from "./components/Features";
import Pricing from "./components/Pricing";
import { api } from "~/utils/api";
@@ -28,13 +29,16 @@ export default function HomeView() {
const isLoggedIn = !!data;
const isDarkMode = theme.activeTheme === "dark";
return (
<>
<style jsx global>{`
html {
scroll-behavior: smooth;
}
body {
background-color: ${theme.theme === "light"
? "hsl(0deg 0% 98.8%)"
: "#161616"};
background-color: ${!isDarkMode ? "hsl(0deg 0% 98.8%)" : "#161616"};
}
`}</style>
<div className="mx-auto flex h-full min-h-screen flex-col items-center bg-light-100 dark:bg-dark-50">
@@ -84,11 +88,11 @@ export default function HomeView() {
</p>
</div>
</div>
<div className="mb-10 rounded-[24px] border border-light-300 bg-light-50 p-2 shadow-md dark:border-dark-300 dark:bg-dark-50">
<div className="mb-24 rounded-[24px] border border-light-300 bg-light-50 p-2 shadow-md dark:border-dark-300 dark:bg-dark-50">
<div className="rounded-[16px] border border-light-300 bg-light-200 p-2 dark:border-dark-300 dark:bg-dark-200">
<div className="overflow-hidden rounded-[16px] shadow-sm">
<Image
src={`/hero-${theme.theme}.png`}
src={`/hero-${isDarkMode ? "dark" : "light"}.png`}
alt="kanban"
width={1100}
height={1000}
@@ -96,7 +100,12 @@ export default function HomeView() {
</div>
</div>
</div>
<div className="pt-10" id="pricing">
<div className="relative pt-10">
<div id="features" className="absolute -top-20" />
<Features theme={theme.activeTheme} />
</div>
<div className="relative pt-10">
<div id="pricing" className="absolute -top-20" />
<Pricing />
</div>
</div>