first commit

This commit is contained in:
SvenFE
2025-07-03 16:42:36 +08:00
commit 887dca8d14
37 changed files with 6707 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
public-hoist-pattern[]=*@heroui/*
package-lock=true

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Next UI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

53
README.md Normal file
View File

@@ -0,0 +1,53 @@
# Next.js & HeroUI Template
This is a template for creating applications using Next.js 14 (app directory) and HeroUI (v2).
[Try it on CodeSandbox](https://githubbox.com/heroui-inc/heroui/next-app-template)
## Technologies Used
- [Next.js 14](https://nextjs.org/docs/getting-started)
- [HeroUI v2](https://heroui.com/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Tailwind Variants](https://tailwind-variants.org)
- [TypeScript](https://www.typescriptlang.org/)
- [Framer Motion](https://www.framer.com/motion/)
- [next-themes](https://github.com/pacocoursey/next-themes)
## How to Use
### Use the template with create-next-app
To create a new project based on this template using `create-next-app`, run the following command:
```bash
npx create-next-app -e https://github.com/heroui-inc/next-app-template
```
### Install dependencies
You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`:
```bash
npm install
```
### Run the development server
```bash
npm run dev
```
### Setup pnpm (optional)
If you are using `pnpm`, you need to add the following code to your `.npmrc` file:
```bash
public-hoist-pattern[]=*@heroui/*
```
After modifying the `.npmrc` file, you need to run `pnpm install` again to ensure that the dependencies are installed correctly.
## License
Licensed under the [MIT license](https://github.com/heroui-inc/next-app-template/blob/main/LICENSE).

13
app/about/layout.tsx Normal file
View File

@@ -0,0 +1,13 @@
export default function AboutLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center">
{children}
</div>
</section>
);
}

9
app/about/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { title } from "@/components/primitives";
export default function AboutPage() {
return (
<div>
<h1 className={title()}>About</h1>
</div>
);
}

13
app/blog/layout.tsx Normal file
View File

@@ -0,0 +1,13 @@
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center">
{children}
</div>
</section>
);
}

9
app/blog/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { title } from "@/components/primitives";
export default function BlogPage() {
return (
<div>
<h1 className={title()}>Blog</h1>
</div>
);
}

13
app/docs/layout.tsx Normal file
View File

@@ -0,0 +1,13 @@
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center">
{children}
</div>
</section>
);
}

9
app/docs/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { title } from "@/components/primitives";
export default function DocsPage() {
return (
<div>
<h1 className={title()}>Docs</h1>
</div>
);
}

31
app/error.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
/* eslint-disable no-console */
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
}

66
app/layout.tsx Normal file
View File

@@ -0,0 +1,66 @@
import "@/styles/globals.css";
import { Metadata, Viewport } from "next";
import { Link } from "@heroui/link";
import clsx from "clsx";
import { Providers } from "./providers";
import { siteConfig } from "@/config/site";
import { fontSans } from "@/config/fonts";
import { Navbar } from "@/components/navbar";
export const metadata: Metadata = {
title: {
default: siteConfig.name,
template: `%s - ${siteConfig.name}`,
},
description: siteConfig.description,
icons: {
icon: "/favicon.ico",
},
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html suppressHydrationWarning lang="en">
<head />
<body
className={clsx(
"min-h-screen text-foreground bg-background font-sans antialiased",
fontSans.variable,
)}
>
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
<div className="relative flex flex-col h-screen">
<Navbar />
<main className="container mx-auto max-w-7xl pt-16 px-6 flex-grow">
{children}
</main>
<footer className="w-full flex items-center justify-center py-3">
<Link
isExternal
className="flex items-center gap-1 text-current"
href="https://heroui.com?utm_source=next-app-template"
title="heroui.com homepage"
>
<span className="text-default-600">Powered by</span>
<p className="text-primary">HeroUI</p>
</Link>
</footer>
</div>
</Providers>
</body>
</html>
);
}

56
app/page.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { Link } from "@heroui/link";
import { Snippet } from "@heroui/snippet";
import { Code } from "@heroui/code";
import { button as buttonStyles } from "@heroui/theme";
import { siteConfig } from "@/config/site";
import { title, subtitle } from "@/components/primitives";
import { GithubIcon } from "@/components/icons";
export default function Home() {
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-xl text-center justify-center">
<span className={title()}>Make&nbsp;</span>
<span className={title({ color: "violet" })}>beautiful&nbsp;</span>
<br />
<span className={title()}>
websites regardless of your design experience.
</span>
<div className={subtitle({ class: "mt-4" })}>
Beautiful, fast and modern React UI library.
</div>
</div>
<div className="flex gap-3">
<Link
isExternal
className={buttonStyles({
color: "primary",
radius: "full",
variant: "shadow",
})}
href={siteConfig.links.docs}
>
Documentation
</Link>
<Link
isExternal
className={buttonStyles({ variant: "bordered", radius: "full" })}
href={siteConfig.links.github}
>
<GithubIcon size={20} />
GitHub
</Link>
</div>
<div className="mt-8">
<Snippet hideCopyButton hideSymbol variant="bordered">
<span>
Get started by editing <Code color="primary">app/page.tsx</Code>
</span>
</Snippet>
</div>
</section>
);
}

13
app/pricing/layout.tsx Normal file
View File

@@ -0,0 +1,13 @@
export default function PricingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center">
{children}
</div>
</section>
);
}

9
app/pricing/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { title } from "@/components/primitives";
export default function PricingPage() {
return (
<div>
<h1 className={title()}>Pricing</h1>
</div>
);
}

31
app/providers.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client";
import type { ThemeProviderProps } from "next-themes";
import * as React from "react";
import { HeroUIProvider } from "@heroui/system";
import { useRouter } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export interface ProvidersProps {
children: React.ReactNode;
themeProps?: ThemeProviderProps;
}
declare module "@react-types/shared" {
interface RouterConfig {
routerOptions: NonNullable<
Parameters<ReturnType<typeof useRouter>["push"]>[1]
>;
}
}
export function Providers({ children, themeProps }: ProvidersProps) {
const router = useRouter();
return (
<HeroUIProvider navigate={router.push}>
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
</HeroUIProvider>
);
}

14
components/counter.tsx Normal file
View File

@@ -0,0 +1,14 @@
"use client";
import { useState } from "react";
import { Button } from "@heroui/button";
export const Counter = () => {
const [count, setCount] = useState(0);
return (
<Button radius="full" onPress={() => setCount(count + 1)}>
Count is {count}
</Button>
);
};

187
components/icons.tsx Normal file
View File

@@ -0,0 +1,187 @@
import * as React from "react";
import { IconSvgProps } from "@/types";
export const Logo: React.FC<IconSvgProps> = ({
size = 36,
width,
height,
...props
}) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 32 32"
width={size || width}
{...props}
>
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export const DiscordIcon: React.FC<IconSvgProps> = ({
size = 24,
width,
height,
...props
}) => {
return (
<svg
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z"
fill="currentColor"
/>
</svg>
);
};
export const TwitterIcon: React.FC<IconSvgProps> = ({
size = 24,
width,
height,
...props
}) => {
return (
<svg
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
fill="currentColor"
/>
</svg>
);
};
export const GithubIcon: React.FC<IconSvgProps> = ({
size = 24,
width,
height,
...props
}) => {
return (
<svg
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
clipRule="evenodd"
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
};
export const MoonFilledIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
fill="currentColor"
/>
</svg>
);
export const SunFilledIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<g fill="currentColor">
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
</g>
</svg>
);
export const HeartFilledIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M12.62 20.81c-.34.12-.9.12-1.24 0C8.48 19.82 2 15.69 2 8.69 2 5.6 4.49 3.1 7.56 3.1c1.82 0 3.43.88 4.44 2.24a5.53 5.53 0 0 1 4.44-2.24C19.51 3.1 22 5.6 22 8.69c0 7-6.48 11.13-9.38 12.12Z"
fill="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</svg>
);
export const SearchIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<path
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
<path
d="M22 22L20 20"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);

141
components/navbar.tsx Normal file
View File

@@ -0,0 +1,141 @@
import {
Navbar as HeroUINavbar,
NavbarContent,
NavbarMenu,
NavbarMenuToggle,
NavbarBrand,
NavbarItem,
NavbarMenuItem,
} from "@heroui/navbar";
import { Button } from "@heroui/button";
import { Kbd } from "@heroui/kbd";
import { Link } from "@heroui/link";
import { Input } from "@heroui/input";
import { link as linkStyles } from "@heroui/theme";
import NextLink from "next/link";
import clsx from "clsx";
import { siteConfig } from "@/config/site";
import { ThemeSwitch } from "@/components/theme-switch";
import {
TwitterIcon,
GithubIcon,
DiscordIcon,
HeartFilledIcon,
SearchIcon,
Logo,
} from "@/components/icons";
export const Navbar = () => {
const searchInput = (
<Input
aria-label="Search"
classNames={{
inputWrapper: "bg-default-100",
input: "text-sm",
}}
endContent={
<Kbd className="hidden lg:inline-block" keys={["command"]}>
K
</Kbd>
}
labelPlacement="outside"
placeholder="Search..."
startContent={
<SearchIcon className="text-base text-default-400 pointer-events-none flex-shrink-0" />
}
type="search"
/>
);
return (
<HeroUINavbar maxWidth="xl" position="sticky">
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
<NavbarBrand as="li" className="gap-3 max-w-fit">
<NextLink className="flex justify-start items-center gap-1" href="/">
<Logo />
<p className="font-bold text-inherit">ACME</p>
</NextLink>
</NavbarBrand>
<ul className="hidden lg:flex gap-4 justify-start ml-2">
{siteConfig.navItems.map((item) => (
<NavbarItem key={item.href}>
<NextLink
className={clsx(
linkStyles({ color: "foreground" }),
"data-[active=true]:text-primary data-[active=true]:font-medium",
)}
color="foreground"
href={item.href}
>
{item.label}
</NextLink>
</NavbarItem>
))}
</ul>
</NavbarContent>
<NavbarContent
className="hidden sm:flex basis-1/5 sm:basis-full"
justify="end"
>
<NavbarItem className="hidden sm:flex gap-2">
<Link isExternal aria-label="Twitter" href={siteConfig.links.twitter}>
<TwitterIcon className="text-default-500" />
</Link>
<Link isExternal aria-label="Discord" href={siteConfig.links.discord}>
<DiscordIcon className="text-default-500" />
</Link>
<Link isExternal aria-label="Github" href={siteConfig.links.github}>
<GithubIcon className="text-default-500" />
</Link>
<ThemeSwitch />
</NavbarItem>
<NavbarItem className="hidden lg:flex">{searchInput}</NavbarItem>
<NavbarItem className="hidden md:flex">
<Button
isExternal
as={Link}
className="text-sm font-normal text-default-600 bg-default-100"
href={siteConfig.links.sponsor}
startContent={<HeartFilledIcon className="text-danger" />}
variant="flat"
>
Sponsor
</Button>
</NavbarItem>
</NavbarContent>
<NavbarContent className="sm:hidden basis-1 pl-4" justify="end">
<Link isExternal aria-label="Github" href={siteConfig.links.github}>
<GithubIcon className="text-default-500" />
</Link>
<ThemeSwitch />
<NavbarMenuToggle />
</NavbarContent>
<NavbarMenu>
{searchInput}
<div className="mx-4 mt-2 flex flex-col gap-2">
{siteConfig.navMenuItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
color={
index === 2
? "primary"
: index === siteConfig.navMenuItems.length - 1
? "danger"
: "foreground"
}
href="#"
size="lg"
>
{item.label}
</Link>
</NavbarMenuItem>
))}
</div>
</NavbarMenu>
</HeroUINavbar>
);
};

53
components/primitives.ts Normal file
View File

@@ -0,0 +1,53 @@
import { tv } from "tailwind-variants";
export const title = tv({
base: "tracking-tight inline font-semibold",
variants: {
color: {
violet: "from-[#FF1CF7] to-[#b249f8]",
yellow: "from-[#FF705B] to-[#FFB457]",
blue: "from-[#5EA2EF] to-[#0072F5]",
cyan: "from-[#00b7fa] to-[#01cfea]",
green: "from-[#6FEE8D] to-[#17c964]",
pink: "from-[#FF72E1] to-[#F54C7A]",
foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
},
size: {
sm: "text-3xl lg:text-4xl",
md: "text-[2.3rem] lg:text-5xl leading-9",
lg: "text-4xl lg:text-6xl",
},
fullWidth: {
true: "w-full block",
},
},
defaultVariants: {
size: "md",
},
compoundVariants: [
{
color: [
"violet",
"yellow",
"blue",
"cyan",
"green",
"pink",
"foreground",
],
class: "bg-clip-text text-transparent bg-gradient-to-b",
},
],
});
export const subtitle = tv({
base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
variants: {
fullWidth: {
true: "!w-full",
},
},
defaultVariants: {
fullWidth: true,
},
});

View File

@@ -0,0 +1,81 @@
"use client";
import { FC } from "react";
import { VisuallyHidden } from "@react-aria/visually-hidden";
import { SwitchProps, useSwitch } from "@heroui/switch";
import { useTheme } from "next-themes";
import { useIsSSR } from "@react-aria/ssr";
import clsx from "clsx";
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
export interface ThemeSwitchProps {
className?: string;
classNames?: SwitchProps["classNames"];
}
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
className,
classNames,
}) => {
const { theme, setTheme } = useTheme();
const isSSR = useIsSSR();
const onChange = () => {
theme === "light" ? setTheme("dark") : setTheme("light");
};
const {
Component,
slots,
isSelected,
getBaseProps,
getInputProps,
getWrapperProps,
} = useSwitch({
isSelected: theme === "light" || isSSR,
"aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
onChange,
});
return (
<Component
{...getBaseProps({
className: clsx(
"px-px transition-opacity hover:opacity-80 cursor-pointer",
className,
classNames?.base,
),
})}
>
<VisuallyHidden>
<input {...getInputProps()} />
</VisuallyHidden>
<div
{...getWrapperProps()}
className={slots.wrapper({
class: clsx(
[
"w-auto h-auto",
"bg-transparent",
"rounded-lg",
"flex items-center justify-center",
"group-data-[selected=true]:bg-transparent",
"!text-default-500",
"pt-px",
"px-0",
"mx-0",
],
classNames?.wrapper,
),
})}
>
{!isSelected || isSSR ? (
<SunFilledIcon size={22} />
) : (
<MoonFilledIcon size={22} />
)}
</div>
</Component>
);
};

11
config/fonts.ts Normal file
View File

@@ -0,0 +1,11 @@
import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google";
export const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export const fontMono = FontMono({
subsets: ["latin"],
variable: "--font-mono",
});

69
config/site.ts Normal file
View File

@@ -0,0 +1,69 @@
export type SiteConfig = typeof siteConfig;
export const siteConfig = {
name: "Next.js + HeroUI",
description: "Make beautiful websites regardless of your design experience.",
navItems: [
{
label: "Home",
href: "/",
},
{
label: "Docs",
href: "/docs",
},
{
label: "Pricing",
href: "/pricing",
},
{
label: "Blog",
href: "/blog",
},
{
label: "About",
href: "/about",
},
],
navMenuItems: [
{
label: "Profile",
href: "/profile",
},
{
label: "Dashboard",
href: "/dashboard",
},
{
label: "Projects",
href: "/projects",
},
{
label: "Team",
href: "/team",
},
{
label: "Calendar",
href: "/calendar",
},
{
label: "Settings",
href: "/settings",
},
{
label: "Help & Feedback",
href: "/help-feedback",
},
{
label: "Logout",
href: "/logout",
},
],
links: {
github: "https://github.com/heroui-inc/heroui",
twitter: "https://twitter.com/hero_ui",
docs: "https://heroui.com",
discord: "https://discord.gg/9b6yyZKmH4",
sponsor: "https://patreon.com/jrgarciadev",
},
};

151
eslint.config.mjs Normal file
View File

@@ -0,0 +1,151 @@
import { defineConfig, globalIgnores } from "eslint/config";
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
import react from "eslint-plugin-react";
import unusedImports from "eslint-plugin-unused-imports";
import _import from "eslint-plugin-import";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import jsxA11Y from "eslint-plugin-jsx-a11y";
import prettier from "eslint-plugin-prettier";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([globalIgnores([
".now/*",
"**/*.css",
"**/.changeset",
"**/dist",
"esm/*",
"public/*",
"tests/*",
"scripts/*",
"**/*.config.js",
"**/.DS_Store",
"**/node_modules",
"**/coverage",
"**/.next",
"**/build",
"!**/.commitlintrc.cjs",
"!**/.lintstagedrc.cjs",
"!**/jest.config.js",
"!**/plopfile.js",
"!**/react-shim.js",
"!**/tsup.config.ts",
]), {
extends: fixupConfigRules(compat.extends(
"plugin:react/recommended",
"plugin:prettier/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:@next/next/recommended",
)),
plugins: {
react: fixupPluginRules(react),
"unused-imports": unusedImports,
import: fixupPluginRules(_import),
"@typescript-eslint": typescriptEslint,
"jsx-a11y": fixupPluginRules(jsxA11Y),
prettier: fixupPluginRules(prettier),
},
languageOptions: {
globals: {
...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
...globals.node,
},
parser: tsParser,
ecmaVersion: 12,
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: "detect",
},
},
files: ["**/*.ts", "**/*.tsx"],
rules: {
"no-console": "warn",
"react/prop-types": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/interactive-supports-focus": "warn",
"prettier/prettier": "warn",
"no-unused-vars": "off",
"unused-imports/no-unused-vars": "off",
"unused-imports/no-unused-imports": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
args: "after-used",
ignoreRestSiblings: false,
argsIgnorePattern: "^_.*?$",
}],
"import/order": ["warn", {
groups: [
"type",
"builtin",
"object",
"external",
"internal",
"parent",
"sibling",
"index",
],
pathGroups: [{
pattern: "~/**",
group: "external",
position: "after",
}],
"newlines-between": "always",
}],
"react/self-closing-comp": "warn",
"react/jsx-sort-props": ["warn", {
callbacksLast: true,
shorthandFirst: true,
noSortAlphabetically: false,
reservedFirst: true,
}],
"padding-line-between-statements": ["warn", {
blankLine: "always",
prev: "*",
next: "return",
}, {
blankLine: "always",
prev: ["const", "let", "var"],
next: "*",
}, {
blankLine: "any",
prev: ["const", "let", "var"],
next: ["const", "let", "var"],
}],
},
}]);

4
next.config.js Normal file
View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;

62
package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "next-app-template",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --fix"
},
"dependencies": {
"@heroui/button": "2.2.22",
"@heroui/code": "2.2.16",
"@heroui/input": "2.4.22",
"@heroui/kbd": "2.2.17",
"@heroui/link": "2.2.19",
"@heroui/listbox": "2.3.21",
"@heroui/navbar": "2.2.20",
"@heroui/snippet": "2.2.23",
"@heroui/switch": "2.2.20",
"@heroui/system": "2.4.18",
"@heroui/theme": "2.4.17",
"@react-aria/ssr": "3.9.9",
"@react-aria/visually-hidden": "3.8.24",
"clsx": "2.1.1",
"framer-motion": "11.13.1",
"intl-messageformat": "10.7.16",
"next": "15.3.1",
"next-themes": "0.4.6",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@eslint/compat": "1.2.8",
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.25.1",
"@next/eslint-plugin-next": "15.3.1",
"@react-types/shared": "3.30.0",
"@types/node": "22.15.3",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "8.31.1",
"@typescript-eslint/parser": "8.31.1",
"autoprefixer": "10.4.21",
"eslint": "9.25.1",
"eslint-config-next": "15.3.1",
"eslint-config-prettier": "10.1.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "5.2.6",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-unused-imports": "4.1.4",
"globals": "16.0.0",
"postcss": "8.5.3",
"prettier": "3.5.3",
"tailwind-variants": "0.3.0",
"tailwindcss": "3.4.16",
"typescript": "5.6.3"
}
}

5478
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- '@heroui/shared-utils'
- sharp
- unrs-resolver

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

3
styles/globals.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

22
tailwind.config.js Normal file
View File

@@ -0,0 +1,22 @@
import {heroui} from "@heroui/theme"
/** @type {import('tailwindcss').Config} */
const config = {
content: [
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)"],
mono: ["var(--font-mono)"],
},
},
},
darkMode: "class",
plugins: [heroui()],
}
module.exports = config;

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

5
types/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import { SVGProps } from "react";
export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number;
};