mirror of
https://github.com/lingke-net/koring-space.git
synced 2026-09-12 05:45:17 +08:00
基础
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+57
@@ -0,0 +1,57 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<HTMLCodeStyleSettings>
|
||||||
|
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
</HTMLCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<VueCodeStyleSettings>
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||||
|
</VueCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<option name="SOFT_MARGINS" value="100" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="SOFT_MARGINS" value="100" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="SOFT_MARGINS" value="100" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Vue">
|
||||||
|
<option name="SOFT_MARGINS" value="100" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
Generated
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/koring-space.iml" filepath="$PROJECT_DIR$/.idea/koring-space.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
singleQuote: false
|
||||||
|
semi: true
|
||||||
|
printWidth: 100
|
||||||
|
trailingComma: none
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--animate-warning-scroll: warning-scroll 1.5s linear infinite;
|
||||||
|
@keyframes warning-scroll {
|
||||||
|
from { background-position: 0 0; }
|
||||||
|
to { background-position: 56.56px 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
--animate-meteor: meteor 5s linear infinite;
|
||||||
|
@keyframes meteor {
|
||||||
|
0% {
|
||||||
|
transform: rotate(var(--angle)) translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(var(--angle)) translateX(-500px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { Header } from "@/layout/header";
|
||||||
|
import { Footer } from "@/layout/footer";
|
||||||
|
import Script from "next/script";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const alimamaFangYuan = localFont({
|
||||||
|
src: "../public/font/Alimama.ttf",
|
||||||
|
variable: "--font-sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "RivFox",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.svg",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={cn("h-full", "antialiased", alimamaFangYuan.variable, geistMono.variable, "font-sans")}
|
||||||
|
>
|
||||||
|
<body className="min-h-full flex flex-col">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1 pt-14 px-4 sm:px-10 lg:px-48">
|
||||||
|
{children}
|
||||||
|
<Script
|
||||||
|
src="https://analysis.rivfox.com/api/script.js"
|
||||||
|
data-site-id="aaedd41de497"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
<Footer/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useNewsStore } from "@/lib/database";
|
||||||
|
|
||||||
|
export default async function NewsPage() {
|
||||||
|
const newsList = await useNewsStore.findAsync({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<h1 className="text-2xl font-bold">新闻动态</h1>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{newsList.length > 0 ? (
|
||||||
|
newsList.map((item: any) => (
|
||||||
|
<div
|
||||||
|
key={item._id}
|
||||||
|
className="p-4 border rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-semibold">{item.title || "无标题"}</h2>
|
||||||
|
<p className="text-gray-500 text-sm mt-2">{item.content || "暂无内容..."}</p>
|
||||||
|
<div className="text-xs text-gray-400 mt-4">
|
||||||
|
发布于: {item.createdAt ? new Date(item.createdAt).toLocaleDateString() : "未知"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">目前还没有新闻内容。</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
||||||
|
import { ArrowUpRight, HammerIcon } from "lucide-react";
|
||||||
|
import { Marquee, MarqueeContent, MarqueeFade, MarqueeItem } from "@/components/kibo-ui/marquee";
|
||||||
|
import { Status, StatusIndicator } from "@/components/kibo-ui/status";
|
||||||
|
import { Meteors } from "@/components/ui/meteors";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-full h-full gap-4 md:gap-8">
|
||||||
|
<div className="relative overflow-hidden rounded-xl border border-amber-500/50 group">
|
||||||
|
{/* 滚动条纹层 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0 animate-warning-scroll opacity-100"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"repeating-linear-gradient(-45deg, #f59e0b, #f59e0b 20px, #000 20px, #000 40px)",
|
||||||
|
backgroundSize: "56.56px 56.56px"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容层 */}
|
||||||
|
<div className="relative z-10 flex items-center justify-center gap-4 p-3 bg-amber-500/80">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 bg-black rounded-full text-amber-500">
|
||||||
|
<HammerIcon className="size-4" />
|
||||||
|
<span className="text-[10px] font-black tracking-widest">正在接受测试</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-black font-bold text-sm tracking-tight">Koring Launcher UI 预览版本已发布</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-full h-120 overflow-hidden">
|
||||||
|
<div className="relative rounded-xl border w-full h-full">
|
||||||
|
<div className="absolute left-12 top-12 flex flex-col gap-2">
|
||||||
|
<span className="font-bold text-5xl">Koring Team</span>
|
||||||
|
<span className="text-xl">创意无限</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="absolute right-12 bottom-12 text-xl text-muted-foreground">
|
||||||
|
与光年相遇,见宇宙未开源的无限
|
||||||
|
<span className="text-sm">
|
||||||
|
<br /> 以星辰为数据,用仰望作读取
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<FlickeringGrid
|
||||||
|
className="relative inset-0 z-0 mask-[radial-gradient(700px_circle_at_center,white,transparent)]"
|
||||||
|
squareSize={6}
|
||||||
|
gridGap={14}
|
||||||
|
color="#6b9eb6"
|
||||||
|
maxOpacity={0.7}
|
||||||
|
flickerChance={0.1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/launcher"
|
||||||
|
className="relative flex w-full flex-col overflow-hidden rounded-md border p-6 group dark text-white bg-[#0A0A0AFF]"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
要不来尝尝鲜?试试这个...
|
||||||
|
<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">Koring Launcher</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">基于 electron 的 Minecraft 启动器</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Meteors number={28} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="py-8 font-bold text-xl">
|
||||||
|
<Marquee>
|
||||||
|
<MarqueeFade side="left" />
|
||||||
|
<MarqueeFade side="right" />
|
||||||
|
<MarqueeContent>
|
||||||
|
<MarqueeItem className="w-32">Launcher</MarqueeItem>
|
||||||
|
<MarqueeItem className="w-32">Account</MarqueeItem>
|
||||||
|
<MarqueeItem className="w-32">Play服务</MarqueeItem>
|
||||||
|
<MarqueeItem className="w-32">木柴.空间</MarqueeItem>
|
||||||
|
<MarqueeItem className="w-32">SkinEditer</MarqueeItem>
|
||||||
|
<MarqueeItem className="w-32">创绘基金</MarqueeItem>
|
||||||
|
</MarqueeContent>
|
||||||
|
</Marquee>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<a
|
||||||
|
href="https://docs.play.lenjing.work"
|
||||||
|
target="_blank"
|
||||||
|
className="group rounded-md border border-border p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<b>散射系列</b>
|
||||||
|
<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">Sanshe Play</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Minecraft 综合性一体化设施平台</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/launcher"
|
||||||
|
className="group rounded-md border border-border p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<b>科灵启动器</b>
|
||||||
|
<Status status="maintenance">
|
||||||
|
<StatusIndicator />
|
||||||
|
UI 预览版
|
||||||
|
</Status>
|
||||||
|
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">Koring Launcher</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">基于 electron 的 Minecraft 启动器</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a className="group rounded-md border border-border p-6">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<b>绘皮编辑器</b>
|
||||||
|
<Status status="degraded">
|
||||||
|
<StatusIndicator />
|
||||||
|
待开发
|
||||||
|
</Status>
|
||||||
|
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">SkinEditer</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">基于 Tauri 的 Minecraft 皮肤编辑器</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a className="group rounded-md border border-border p-6">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<b>绘皮编辑器</b>
|
||||||
|
<Status status="degraded">
|
||||||
|
<StatusIndicator />
|
||||||
|
封测中
|
||||||
|
</Status>
|
||||||
|
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">SkinEditer</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">基于 Tauri 的 Minecraft 皮肤编辑器</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a className="group rounded-md border border-border p-6">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<b>创汇基金</b>
|
||||||
|
<Status status="degraded">
|
||||||
|
<StatusIndicator />
|
||||||
|
邀请制
|
||||||
|
</Status>
|
||||||
|
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">Koring Foreign Exchange Fund</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">100万 RMB 的创造力奖励基金</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://mchine.space"
|
||||||
|
target="_blank"
|
||||||
|
className="group rounded-md border border-border p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<b>木柴.空间</b>
|
||||||
|
<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1 font-semibold">MChine Space</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Minecraft 创作者赞助平台</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useNewsStore } from "@/lib/database";
|
||||||
|
import NewsEditor from "@/components/news-editor";
|
||||||
|
|
||||||
|
export default async function NewsPage() {
|
||||||
|
const newsList = await useNewsStore.findAsync({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-[calc(100vh-25vh)]">
|
||||||
|
<div className="flex-1">213</div>
|
||||||
|
|
||||||
|
<NewsEditor className="max-h-52" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "radix-nova",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {
|
||||||
|
"@magicui": "https://magicui.design/r/{name}",
|
||||||
|
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||||
|
import { type LucideIcon, XIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
type ComponentProps,
|
||||||
|
createContext,
|
||||||
|
type HTMLAttributes,
|
||||||
|
type MouseEventHandler,
|
||||||
|
useContext,
|
||||||
|
} from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type BannerContextProps = {
|
||||||
|
show: boolean;
|
||||||
|
setShow: (show: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BannerContext = createContext<BannerContextProps>({
|
||||||
|
show: true,
|
||||||
|
setShow: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BannerProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
visible?: boolean;
|
||||||
|
defaultVisible?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Banner = ({
|
||||||
|
children,
|
||||||
|
visible,
|
||||||
|
defaultVisible = true,
|
||||||
|
onClose,
|
||||||
|
className,
|
||||||
|
inset = false,
|
||||||
|
...props
|
||||||
|
}: BannerProps) => {
|
||||||
|
const [show, setShow] = useControllableState({
|
||||||
|
defaultProp: defaultVisible,
|
||||||
|
prop: visible,
|
||||||
|
onChange: onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BannerContext.Provider value={{ show, setShow }}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center justify-between gap-2 bg-primary px-4 py-2 text-primary-foreground",
|
||||||
|
inset && "rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</BannerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BannerIconProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
icon: LucideIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BannerIcon = ({
|
||||||
|
icon: Icon,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BannerIconProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-full border border-background/20 bg-background/10 p-1 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Icon size={16} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type BannerTitleProps = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
|
export const BannerTitle = ({ className, ...props }: BannerTitleProps) => (
|
||||||
|
<p className={cn("flex-1 text-sm", className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type BannerActionProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const BannerAction = ({
|
||||||
|
variant = "outline",
|
||||||
|
size = "sm",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BannerActionProps) => (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-transparent hover:bg-background/10 hover:text-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
size={size}
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type BannerCloseProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const BannerClose = ({
|
||||||
|
variant = "ghost",
|
||||||
|
size = "icon",
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BannerCloseProps) => {
|
||||||
|
const { setShow } = useContext(BannerContext);
|
||||||
|
|
||||||
|
const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
setShow(false);
|
||||||
|
onClick?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-transparent hover:bg-background/10 hover:text-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
size={size}
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<XIcon size={18} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { HTMLAttributes } from "react";
|
||||||
|
import type { MarqueeProps as FastMarqueeProps } from "react-fast-marquee";
|
||||||
|
import FastMarquee from "react-fast-marquee";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type MarqueeProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const Marquee = ({ className, ...props }: MarqueeProps) => (
|
||||||
|
<div
|
||||||
|
className={cn("relative w-full overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MarqueeContentProps = FastMarqueeProps;
|
||||||
|
|
||||||
|
export const MarqueeContent = ({
|
||||||
|
loop = 0,
|
||||||
|
autoFill = true,
|
||||||
|
pauseOnHover = true,
|
||||||
|
...props
|
||||||
|
}: MarqueeContentProps) => (
|
||||||
|
<FastMarquee
|
||||||
|
autoFill={autoFill}
|
||||||
|
loop={loop}
|
||||||
|
pauseOnHover={pauseOnHover}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MarqueeFadeProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
side: "left" | "right";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarqueeFade = ({
|
||||||
|
className,
|
||||||
|
side,
|
||||||
|
...props
|
||||||
|
}: MarqueeFadeProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0 bottom-0 z-10 h-full w-24 from-background to-transparent",
|
||||||
|
side === "left" ? "left-0 bg-gradient-to-r" : "right-0 bg-gradient-to-l",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MarqueeItemProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const MarqueeItem = ({ className, ...props }: MarqueeItemProps) => (
|
||||||
|
<div
|
||||||
|
className={cn("mx-2 flex-shrink-0 object-contain", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { ComponentProps, HTMLAttributes } from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type StatusProps = ComponentProps<typeof Badge> & {
|
||||||
|
status: "online" | "offline" | "maintenance" | "degraded";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Status = ({ className, status, ...props }: StatusProps) => (
|
||||||
|
<Badge
|
||||||
|
className={cn("flex items-center gap-2", "group", status, className)}
|
||||||
|
variant="secondary"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type StatusIndicatorProps = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
export const StatusIndicator = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: StatusIndicatorProps) => (
|
||||||
|
<span className="relative flex h-2 w-2" {...props}>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
|
||||||
|
"group-[.online]:bg-emerald-500",
|
||||||
|
"group-[.offline]:bg-red-500",
|
||||||
|
"group-[.maintenance]:bg-blue-500",
|
||||||
|
"group-[.degraded]:bg-amber-500"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex h-2 w-2 rounded-full",
|
||||||
|
"group-[.online]:bg-emerald-500",
|
||||||
|
"group-[.offline]:bg-red-500",
|
||||||
|
"group-[.maintenance]:bg-blue-500",
|
||||||
|
"group-[.degraded]:bg-amber-500"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type StatusLabelProps = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
export const StatusLabel = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: StatusLabelProps) => (
|
||||||
|
<span className={cn("text-muted-foreground", className)} {...props}>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<span className="hidden group-[.online]:block">Online</span>
|
||||||
|
<span className="hidden group-[.offline]:block">Offline</span>
|
||||||
|
<span className="hidden group-[.maintenance]:block">Maintenance</span>
|
||||||
|
<span className="hidden group-[.degraded]:block">Degraded</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Editor, JSONContent } from "@/components/kibo-ui/editor";
|
||||||
|
import {
|
||||||
|
EditorBubbleMenu,
|
||||||
|
EditorCharacterCount,
|
||||||
|
EditorClearFormatting,
|
||||||
|
EditorFloatingMenu,
|
||||||
|
EditorFormatBold,
|
||||||
|
EditorFormatCode,
|
||||||
|
EditorFormatItalic,
|
||||||
|
EditorFormatStrike,
|
||||||
|
EditorFormatSubscript,
|
||||||
|
EditorFormatSuperscript,
|
||||||
|
EditorFormatUnderline,
|
||||||
|
EditorLinkSelector,
|
||||||
|
EditorNodeBulletList,
|
||||||
|
EditorNodeCode,
|
||||||
|
EditorNodeHeading1,
|
||||||
|
EditorNodeHeading2,
|
||||||
|
EditorNodeHeading3,
|
||||||
|
EditorNodeOrderedList,
|
||||||
|
EditorNodeQuote,
|
||||||
|
EditorNodeTable,
|
||||||
|
EditorNodeTaskList,
|
||||||
|
EditorNodeText,
|
||||||
|
EditorProvider,
|
||||||
|
EditorSelector,
|
||||||
|
EditorTableColumnAfter,
|
||||||
|
EditorTableColumnBefore,
|
||||||
|
EditorTableColumnDelete,
|
||||||
|
EditorTableColumnMenu,
|
||||||
|
EditorTableDelete,
|
||||||
|
EditorTableFix,
|
||||||
|
EditorTableGlobalMenu,
|
||||||
|
EditorTableHeaderColumnToggle,
|
||||||
|
EditorTableHeaderRowToggle,
|
||||||
|
EditorTableMenu,
|
||||||
|
EditorTableMergeCells,
|
||||||
|
EditorTableRowAfter,
|
||||||
|
EditorTableRowBefore,
|
||||||
|
EditorTableRowDelete,
|
||||||
|
EditorTableRowMenu,
|
||||||
|
EditorTableSplitCell
|
||||||
|
} from "@/components/kibo-ui/editor";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface NewsEditorProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewsEditor({ className }: NewsEditorProps) {
|
||||||
|
const [content, setContent] = useState<JSONContent>({
|
||||||
|
type: "doc",
|
||||||
|
content: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdate = ({ editor }: { editor: Editor }) => {
|
||||||
|
const json = editor.getJSON();
|
||||||
|
|
||||||
|
setContent(json);
|
||||||
|
console.log(JSON.stringify(json));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorProvider
|
||||||
|
className={cn("h-full w-full overflow-y-auto rounded-lg border bg-background p-4", className)}
|
||||||
|
content={content}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
placeholder="Start typing..."
|
||||||
|
>
|
||||||
|
<EditorFloatingMenu>
|
||||||
|
<EditorNodeHeading1 hideName />
|
||||||
|
<EditorNodeBulletList hideName />
|
||||||
|
<EditorNodeQuote hideName />
|
||||||
|
<EditorNodeCode hideName />
|
||||||
|
<EditorNodeTable hideName />
|
||||||
|
</EditorFloatingMenu>
|
||||||
|
<EditorBubbleMenu>
|
||||||
|
<EditorSelector title="Text">
|
||||||
|
<EditorNodeText />
|
||||||
|
<EditorNodeHeading1 />
|
||||||
|
<EditorNodeHeading2 />
|
||||||
|
<EditorNodeHeading3 />
|
||||||
|
<EditorNodeBulletList />
|
||||||
|
<EditorNodeOrderedList />
|
||||||
|
<EditorNodeTaskList />
|
||||||
|
<EditorNodeQuote />
|
||||||
|
<EditorNodeCode />
|
||||||
|
</EditorSelector>
|
||||||
|
<EditorSelector title="Format">
|
||||||
|
<EditorFormatBold />
|
||||||
|
<EditorFormatItalic />
|
||||||
|
<EditorFormatUnderline />
|
||||||
|
<EditorFormatStrike />
|
||||||
|
<EditorFormatCode />
|
||||||
|
<EditorFormatSuperscript />
|
||||||
|
<EditorFormatSubscript />
|
||||||
|
</EditorSelector>
|
||||||
|
<EditorLinkSelector />
|
||||||
|
<EditorClearFormatting />
|
||||||
|
</EditorBubbleMenu>
|
||||||
|
<EditorTableMenu>
|
||||||
|
<EditorTableColumnMenu>
|
||||||
|
<EditorTableColumnBefore />
|
||||||
|
<EditorTableColumnAfter />
|
||||||
|
<EditorTableColumnDelete />
|
||||||
|
</EditorTableColumnMenu>
|
||||||
|
<EditorTableRowMenu>
|
||||||
|
<EditorTableRowBefore />
|
||||||
|
<EditorTableRowAfter />
|
||||||
|
<EditorTableRowDelete />
|
||||||
|
</EditorTableRowMenu>
|
||||||
|
<EditorTableGlobalMenu>
|
||||||
|
<EditorTableHeaderColumnToggle />
|
||||||
|
<EditorTableHeaderRowToggle />
|
||||||
|
<EditorTableDelete />
|
||||||
|
<EditorTableMergeCells />
|
||||||
|
<EditorTableSplitCell />
|
||||||
|
<EditorTableFix />
|
||||||
|
</EditorTableGlobalMenu>
|
||||||
|
</EditorTableMenu>
|
||||||
|
<EditorCharacterCount.Words>Words: </EditorCharacterCount.Words>
|
||||||
|
</EditorProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
icon: "size-8",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm":
|
||||||
|
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||||
|
"icon-lg": "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
} from "@/components/ui/input-group"
|
||||||
|
import { SearchIcon, CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
||||||
|
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className={cn("py-6 text-center text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||||
|
</CommandPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
size="icon-sm"
|
||||||
|
>
|
||||||
|
<XIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-none font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { CheckIcon, ChevronRightIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
align = "start",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||||
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||||
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
squareSize?: number
|
||||||
|
gridGap?: number
|
||||||
|
flickerChance?: number
|
||||||
|
color?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
className?: string
|
||||||
|
maxOpacity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||||
|
squareSize = 4,
|
||||||
|
gridGap = 6,
|
||||||
|
flickerChance = 0.3,
|
||||||
|
color = "rgb(0, 0, 0)",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
className,
|
||||||
|
maxOpacity = 0.3,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [isInView, setIsInView] = useState(false)
|
||||||
|
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 })
|
||||||
|
|
||||||
|
const memoizedColor = useMemo(() => {
|
||||||
|
const toRGBA = (color: string) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return `rgba(0, 0, 0,`
|
||||||
|
}
|
||||||
|
const canvas = document.createElement("canvas")
|
||||||
|
canvas.width = canvas.height = 1
|
||||||
|
const ctx = canvas.getContext("2d")
|
||||||
|
if (!ctx) return "rgba(255, 0, 0,"
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.fillRect(0, 0, 1, 1)
|
||||||
|
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
|
||||||
|
return `rgba(${r}, ${g}, ${b},`
|
||||||
|
}
|
||||||
|
return toRGBA(color)
|
||||||
|
}, [color])
|
||||||
|
|
||||||
|
const setupCanvas = useCallback(
|
||||||
|
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
canvas.style.width = `${width}px`
|
||||||
|
canvas.style.height = `${height}px`
|
||||||
|
const cols = Math.ceil(width / (squareSize + gridGap))
|
||||||
|
const rows = Math.ceil(height / (squareSize + gridGap))
|
||||||
|
|
||||||
|
const squares = new Float32Array(cols * rows)
|
||||||
|
for (let i = 0; i < squares.length; i++) {
|
||||||
|
squares[i] = Math.random() * maxOpacity
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cols, rows, squares, dpr }
|
||||||
|
},
|
||||||
|
[squareSize, gridGap, maxOpacity]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateSquares = useCallback(
|
||||||
|
(squares: Float32Array, deltaTime: number) => {
|
||||||
|
for (let i = 0; i < squares.length; i++) {
|
||||||
|
if (Math.random() < flickerChance * deltaTime) {
|
||||||
|
squares[i] = Math.random() * maxOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[flickerChance, maxOpacity]
|
||||||
|
)
|
||||||
|
|
||||||
|
const drawGrid = useCallback(
|
||||||
|
(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
squares: Float32Array,
|
||||||
|
dpr: number
|
||||||
|
) => {
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
ctx.fillStyle = "transparent"
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
for (let j = 0; j < rows; j++) {
|
||||||
|
const opacity = squares[i * rows + j]
|
||||||
|
ctx.fillStyle = `${memoizedColor}${opacity})`
|
||||||
|
ctx.fillRect(
|
||||||
|
i * (squareSize + gridGap) * dpr,
|
||||||
|
j * (squareSize + gridGap) * dpr,
|
||||||
|
squareSize * dpr,
|
||||||
|
squareSize * dpr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[memoizedColor, squareSize, gridGap]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const container = containerRef.current
|
||||||
|
const ctx = canvas?.getContext("2d") ?? null
|
||||||
|
let animationFrameId: number | null = null
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
let intersectionObserver: IntersectionObserver | null = null
|
||||||
|
let gridParams: ReturnType<typeof setupCanvas> | null = null
|
||||||
|
|
||||||
|
if (canvas && container && ctx) {
|
||||||
|
const updateCanvasSize = () => {
|
||||||
|
const newWidth = width || container.clientWidth
|
||||||
|
const newHeight = height || container.clientHeight
|
||||||
|
setCanvasSize({ width: newWidth, height: newHeight })
|
||||||
|
gridParams = setupCanvas(canvas, newWidth, newHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCanvasSize()
|
||||||
|
|
||||||
|
let lastTime = 0
|
||||||
|
const animate = (time: number) => {
|
||||||
|
if (!isInView || !gridParams) return
|
||||||
|
|
||||||
|
const deltaTime = (time - lastTime) / 1000
|
||||||
|
lastTime = time
|
||||||
|
|
||||||
|
updateSquares(gridParams.squares, deltaTime)
|
||||||
|
drawGrid(
|
||||||
|
ctx,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
gridParams.cols,
|
||||||
|
gridParams.rows,
|
||||||
|
gridParams.squares,
|
||||||
|
gridParams.dpr
|
||||||
|
)
|
||||||
|
animationFrameId = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateCanvasSize()
|
||||||
|
})
|
||||||
|
resizeObserver.observe(container)
|
||||||
|
|
||||||
|
intersectionObserver = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
setIsInView(entry.isIntersecting)
|
||||||
|
},
|
||||||
|
{ threshold: 0 }
|
||||||
|
)
|
||||||
|
intersectionObserver.observe(canvas)
|
||||||
|
|
||||||
|
if (isInView) {
|
||||||
|
animationFrameId = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId)
|
||||||
|
}
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
if (intersectionObserver) {
|
||||||
|
intersectionObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={cn(`h-full w-full ${className}`)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="pointer-events-none"
|
||||||
|
style={{
|
||||||
|
width: canvasSize.width,
|
||||||
|
height: canvasSize.height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useLayoutEffect, useRef } from "react"
|
||||||
|
import type React from "react"
|
||||||
|
import { useInView } from "motion/react"
|
||||||
|
import { annotate } from "rough-notation"
|
||||||
|
import { type RoughAnnotation } from "rough-notation/lib/model"
|
||||||
|
|
||||||
|
type AnnotationAction =
|
||||||
|
| "highlight"
|
||||||
|
| "underline"
|
||||||
|
| "box"
|
||||||
|
| "circle"
|
||||||
|
| "strike-through"
|
||||||
|
| "crossed-off"
|
||||||
|
| "bracket"
|
||||||
|
|
||||||
|
interface HighlighterProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
action?: AnnotationAction
|
||||||
|
color?: string
|
||||||
|
strokeWidth?: number
|
||||||
|
animationDuration?: number
|
||||||
|
iterations?: number
|
||||||
|
padding?: number
|
||||||
|
multiline?: boolean
|
||||||
|
isView?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Highlighter({
|
||||||
|
children,
|
||||||
|
action = "highlight",
|
||||||
|
color = "#ffd1dc",
|
||||||
|
strokeWidth = 1.5,
|
||||||
|
animationDuration = 600,
|
||||||
|
iterations = 2,
|
||||||
|
padding = 2,
|
||||||
|
multiline = true,
|
||||||
|
isView = false,
|
||||||
|
}: HighlighterProps) {
|
||||||
|
const elementRef = useRef<HTMLSpanElement>(null)
|
||||||
|
|
||||||
|
const isInView = useInView(elementRef, {
|
||||||
|
once: true,
|
||||||
|
margin: "-10%",
|
||||||
|
})
|
||||||
|
|
||||||
|
// If isView is false, always show. If isView is true, wait for inView
|
||||||
|
const shouldShow = !isView || isInView
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const element = elementRef.current
|
||||||
|
let annotation: RoughAnnotation | null = null
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
if (shouldShow && element) {
|
||||||
|
const annotationConfig = {
|
||||||
|
type: action,
|
||||||
|
color,
|
||||||
|
strokeWidth,
|
||||||
|
animationDuration,
|
||||||
|
iterations,
|
||||||
|
padding,
|
||||||
|
multiline,
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAnnotation = annotate(element, annotationConfig)
|
||||||
|
annotation = currentAnnotation
|
||||||
|
currentAnnotation.show()
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
currentAnnotation.hide()
|
||||||
|
currentAnnotation.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
resizeObserver.observe(element)
|
||||||
|
resizeObserver.observe(document.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
annotation?.remove()
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
shouldShow,
|
||||||
|
action,
|
||||||
|
color,
|
||||||
|
strokeWidth,
|
||||||
|
animationDuration,
|
||||||
|
iterations,
|
||||||
|
padding,
|
||||||
|
multiline,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={elementRef} className="relative inline-block bg-transparent">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
|
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-group"
|
||||||
|
role="group"
|
||||||
|
className={cn(
|
||||||
|
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputGroupAddonVariants = cva(
|
||||||
|
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
align: {
|
||||||
|
"inline-start":
|
||||||
|
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
|
||||||
|
"inline-end":
|
||||||
|
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
|
||||||
|
"block-start":
|
||||||
|
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
||||||
|
"block-end":
|
||||||
|
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
align: "inline-start",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function InputGroupAddon({
|
||||||
|
className,
|
||||||
|
align = "inline-start",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="input-group-addon"
|
||||||
|
data-align={align}
|
||||||
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if ((e.target as HTMLElement).closest("button")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputGroupButtonVariants = cva(
|
||||||
|
"flex items-center gap-2 text-sm shadow-none",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||||
|
sm: "",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
||||||
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "xs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function InputGroupButton({
|
||||||
|
className,
|
||||||
|
type = "button",
|
||||||
|
variant = "ghost",
|
||||||
|
size = "xs",
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
VariantProps<typeof inputGroupButtonVariants>) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
data-size={size}
|
||||||
|
variant={variant}
|
||||||
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupTextarea({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupButton,
|
||||||
|
InputGroupText,
|
||||||
|
InputGroupInput,
|
||||||
|
InputGroupTextarea,
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface MeteorsProps {
|
||||||
|
number?: number
|
||||||
|
minDelay?: number
|
||||||
|
maxDelay?: number
|
||||||
|
minDuration?: number
|
||||||
|
maxDuration?: number
|
||||||
|
angle?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Meteors = ({
|
||||||
|
number = 20,
|
||||||
|
minDelay = 0.2,
|
||||||
|
maxDelay = 1.2,
|
||||||
|
minDuration = 2,
|
||||||
|
maxDuration = 10,
|
||||||
|
angle = 215,
|
||||||
|
className,
|
||||||
|
}: MeteorsProps) => {
|
||||||
|
const [meteorStyles, setMeteorStyles] = useState<Array<React.CSSProperties>>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const styles = [...new Array(number)].map(() => ({
|
||||||
|
"--angle": -angle + "deg",
|
||||||
|
top: "-5%",
|
||||||
|
left: `calc(0% + ${Math.floor(Math.random() * window.innerWidth)}px)`,
|
||||||
|
animationDelay: Math.random() * (maxDelay - minDelay) + minDelay + "s",
|
||||||
|
animationDuration:
|
||||||
|
Math.floor(Math.random() * (maxDuration - minDuration) + minDuration) +
|
||||||
|
"s",
|
||||||
|
}))
|
||||||
|
setMeteorStyles(styles)
|
||||||
|
}, [number, minDelay, maxDelay, minDuration, maxDuration, angle])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{[...meteorStyles].map((style, idx) => (
|
||||||
|
// Meteor Head
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
style={{ ...style }}
|
||||||
|
className={cn(
|
||||||
|
"animate-meteor pointer-events-none absolute size-0.5 rotate-(--angle) rounded-full bg-zinc-500 shadow-[0_0_0_1px_#ffffff10]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Meteor Tail */}
|
||||||
|
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-12.5 -translate-y-1/2 bg-linear-to-r from-zinc-500 to-transparent" />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Popover as PopoverPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-header"
|
||||||
|
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-title"
|
||||||
|
className={cn("font-heading font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="popover-description"
|
||||||
|
className={cn("text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverDescription,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTitle,
|
||||||
|
PopoverTrigger,
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { memo } from "react"
|
||||||
|
import {
|
||||||
|
AnimatePresence,
|
||||||
|
motion,
|
||||||
|
Variants,
|
||||||
|
type DOMMotionComponents,
|
||||||
|
type MotionProps,
|
||||||
|
} from "motion/react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
type AnimationType = "text" | "word" | "character" | "line"
|
||||||
|
type AnimationVariant =
|
||||||
|
| "fadeIn"
|
||||||
|
| "blurIn"
|
||||||
|
| "blurInUp"
|
||||||
|
| "blurInDown"
|
||||||
|
| "slideUp"
|
||||||
|
| "slideDown"
|
||||||
|
| "slideLeft"
|
||||||
|
| "slideRight"
|
||||||
|
| "scaleUp"
|
||||||
|
| "scaleDown"
|
||||||
|
|
||||||
|
const motionElements = {
|
||||||
|
article: motion.article,
|
||||||
|
div: motion.div,
|
||||||
|
h1: motion.h1,
|
||||||
|
h2: motion.h2,
|
||||||
|
h3: motion.h3,
|
||||||
|
h4: motion.h4,
|
||||||
|
h5: motion.h5,
|
||||||
|
h6: motion.h6,
|
||||||
|
li: motion.li,
|
||||||
|
p: motion.p,
|
||||||
|
section: motion.section,
|
||||||
|
span: motion.span,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type MotionElementType = Extract<
|
||||||
|
keyof DOMMotionComponents,
|
||||||
|
keyof typeof motionElements
|
||||||
|
>
|
||||||
|
|
||||||
|
interface TextAnimateProps extends Omit<MotionProps, "children"> {
|
||||||
|
/**
|
||||||
|
* The text content to animate
|
||||||
|
*/
|
||||||
|
children: string
|
||||||
|
/**
|
||||||
|
* The class name to be applied to the component
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
|
/**
|
||||||
|
* The class name to be applied to each segment
|
||||||
|
*/
|
||||||
|
segmentClassName?: string
|
||||||
|
/**
|
||||||
|
* The delay before the animation starts
|
||||||
|
*/
|
||||||
|
delay?: number
|
||||||
|
/**
|
||||||
|
* The duration of the animation
|
||||||
|
*/
|
||||||
|
duration?: number
|
||||||
|
/**
|
||||||
|
* Custom motion variants for the animation
|
||||||
|
*/
|
||||||
|
variants?: Variants
|
||||||
|
/**
|
||||||
|
* The element type to render
|
||||||
|
*/
|
||||||
|
as?: MotionElementType
|
||||||
|
/**
|
||||||
|
* How to split the text ("text", "word", "character")
|
||||||
|
*/
|
||||||
|
by?: AnimationType
|
||||||
|
/**
|
||||||
|
* Whether to start animation when component enters viewport
|
||||||
|
*/
|
||||||
|
startOnView?: boolean
|
||||||
|
/**
|
||||||
|
* Whether to animate only once
|
||||||
|
*/
|
||||||
|
once?: boolean
|
||||||
|
/**
|
||||||
|
* The animation preset to use
|
||||||
|
*/
|
||||||
|
animation?: AnimationVariant
|
||||||
|
/**
|
||||||
|
* Whether to enable accessibility features (default: true)
|
||||||
|
*/
|
||||||
|
accessible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const staggerTimings: Record<AnimationType, number> = {
|
||||||
|
text: 0.06,
|
||||||
|
word: 0.05,
|
||||||
|
character: 0.03,
|
||||||
|
line: 0.06,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultContainerVariants = {
|
||||||
|
hidden: { opacity: 1 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
delayChildren: 0,
|
||||||
|
staggerChildren: 0.05,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.05,
|
||||||
|
staggerDirection: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultItemVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultItemAnimationVariants: Record<
|
||||||
|
AnimationVariant,
|
||||||
|
{ container: Variants; item: Variants }
|
||||||
|
> = {
|
||||||
|
fadeIn: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 20,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blurIn: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { opacity: 0, filter: "blur(10px)" },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
filter: "blur(10px)",
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blurInUp: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { opacity: 0, filter: "blur(10px)", y: 20 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
y: { duration: 0.3 },
|
||||||
|
opacity: { duration: 0.4 },
|
||||||
|
filter: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
filter: "blur(10px)",
|
||||||
|
y: 20,
|
||||||
|
transition: {
|
||||||
|
y: { duration: 0.3 },
|
||||||
|
opacity: { duration: 0.4 },
|
||||||
|
filter: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blurInDown: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { opacity: 0, filter: "blur(10px)", y: -20 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
y: { duration: 0.3 },
|
||||||
|
opacity: { duration: 0.4 },
|
||||||
|
filter: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
show: {
|
||||||
|
y: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
y: -20,
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slideDown: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { y: -20, opacity: 0 },
|
||||||
|
show: {
|
||||||
|
y: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
y: 20,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slideLeft: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { x: 20, opacity: 0 },
|
||||||
|
show: {
|
||||||
|
x: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
x: -20,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slideRight: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { x: -20, opacity: 0 },
|
||||||
|
show: {
|
||||||
|
x: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
x: 20,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scaleUp: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { scale: 0.5, opacity: 0 },
|
||||||
|
show: {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
scale: {
|
||||||
|
type: "spring",
|
||||||
|
damping: 15,
|
||||||
|
stiffness: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
scale: 0.5,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scaleDown: {
|
||||||
|
container: defaultContainerVariants,
|
||||||
|
item: {
|
||||||
|
hidden: { scale: 1.5, opacity: 0 },
|
||||||
|
show: {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
scale: {
|
||||||
|
type: "spring",
|
||||||
|
damping: 15,
|
||||||
|
stiffness: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
scale: 1.5,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextAnimateBase = ({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
duration = 0.3,
|
||||||
|
variants,
|
||||||
|
className,
|
||||||
|
segmentClassName,
|
||||||
|
as: Component = "p",
|
||||||
|
startOnView = true,
|
||||||
|
once = false,
|
||||||
|
by = "word",
|
||||||
|
animation = "fadeIn",
|
||||||
|
accessible = true,
|
||||||
|
...props
|
||||||
|
}: TextAnimateProps) => {
|
||||||
|
const MotionComponent = motionElements[Component]
|
||||||
|
|
||||||
|
let segments: string[] = []
|
||||||
|
switch (by) {
|
||||||
|
case "word":
|
||||||
|
segments = children.split(/(\s+)/)
|
||||||
|
break
|
||||||
|
case "character":
|
||||||
|
segments = children.split("")
|
||||||
|
break
|
||||||
|
case "line":
|
||||||
|
segments = children.split("\n")
|
||||||
|
break
|
||||||
|
case "text":
|
||||||
|
default:
|
||||||
|
segments = [children]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalVariants = variants
|
||||||
|
? {
|
||||||
|
container: {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: 0.01, delay },
|
||||||
|
delayChildren: delay,
|
||||||
|
staggerChildren: duration / segments.length,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: duration / segments.length,
|
||||||
|
staggerDirection: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
item: variants,
|
||||||
|
}
|
||||||
|
: animation
|
||||||
|
? {
|
||||||
|
container: {
|
||||||
|
...defaultItemAnimationVariants[animation].container,
|
||||||
|
show: {
|
||||||
|
...defaultItemAnimationVariants[animation].container.show,
|
||||||
|
transition: {
|
||||||
|
delayChildren: delay,
|
||||||
|
staggerChildren: duration / segments.length,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
...defaultItemAnimationVariants[animation].container.exit,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: duration / segments.length,
|
||||||
|
staggerDirection: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
item: defaultItemAnimationVariants[animation].item,
|
||||||
|
}
|
||||||
|
: { container: defaultContainerVariants, item: defaultItemVariants }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
<MotionComponent
|
||||||
|
variants={finalVariants.container as Variants}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView={startOnView ? "show" : undefined}
|
||||||
|
animate={startOnView ? undefined : "show"}
|
||||||
|
exit="exit"
|
||||||
|
className={cn("whitespace-pre-wrap", className)}
|
||||||
|
viewport={{ once }}
|
||||||
|
aria-label={accessible ? children : undefined}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{accessible && <span className="sr-only">{children}</span>}
|
||||||
|
{segments.map((segment, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={`${by}-${segment}-${i}`}
|
||||||
|
variants={finalVariants.item}
|
||||||
|
custom={i * staggerTimings[by]}
|
||||||
|
className={cn(
|
||||||
|
by === "line" ? "block" : "inline-block whitespace-pre",
|
||||||
|
by === "character" && "",
|
||||||
|
segmentClassName
|
||||||
|
)}
|
||||||
|
aria-hidden={accessible ? true : undefined}
|
||||||
|
>
|
||||||
|
{segment}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</MotionComponent>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the memoized version
|
||||||
|
export const TextAnimate = memo(TextAnimateBase)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
return (
|
||||||
|
<div className="py-10 px-4 mt-10 mx-4 md:mx-6 lg:mx-44 flex items-start md:items-center flex-col md:flex-row sm:justify-between gap-4 md:gap-0 border-t">
|
||||||
|
<Link href="/">
|
||||||
|
<Button className="font-bold" variant="ghost">
|
||||||
|
<span>Koring Team Space</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
|
<a href="mailto:hi@lenjing.email?subject=联系支持" target="_blank">
|
||||||
|
<Button variant="ghost">hi@lenjing.email</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||||
|
<Button variant="ghost">鲁ICP备2024089677号-2</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://rivfox.com/" target="_blank">
|
||||||
|
<Button variant="ghost">RIVFOX.COM</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://lingke.ink/" target="_blank">
|
||||||
|
<Button variant="ghost">瓴克科技</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://dream-pep.cn/" target="_blank">
|
||||||
|
<Button variant="ghost">追梦者</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://lenjing.cn/" target="_blank">
|
||||||
|
<Button variant="ghost">棱镜视界</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Footer };
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<div className="fixed w-full backdrop-blur-sm bg-background/70 p-3 px-4 md:px-10 lg:px-48 flex items-center justify-between z-20">
|
||||||
|
<Link href="/">
|
||||||
|
<Button className="font-bold" variant="ghost">
|
||||||
|
Koring Team Space
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<a href="https://support.lingke.ink">
|
||||||
|
<Button variant="ghost">支持</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://learn.rivfox.com">
|
||||||
|
<Button variant="ghost">文档</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Header };
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import Datastore from "@seald-io/nedb";
|
||||||
|
|
||||||
|
const useNewsStore = new Datastore({
|
||||||
|
filename: "./database/news.db",
|
||||||
|
autoload: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export { useNewsStore };
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const { pathname } = request.nextUrl
|
||||||
|
|
||||||
|
if (pathname.startsWith("/rivfox-admin")) {
|
||||||
|
const isAdmin = request.cookies.get("rivfox-admin")?.value === "a2zFc9YTHpgiOxRH4"
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return NextResponse.rewrite(new URL("/404", request.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: "/rivfox-admin/:path*",
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "rivfox.website",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbo",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.7.6",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
|
"@seald-io/nedb": "^4.1.2",
|
||||||
|
"@tiptap/core": "^3.22.1",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.22.1",
|
||||||
|
"@tiptap/extension-list": "^3.22.1",
|
||||||
|
"@tiptap/extension-subscript": "^3.22.1",
|
||||||
|
"@tiptap/extension-superscript": "^3.22.1",
|
||||||
|
"@tiptap/extension-table": "^3.22.1",
|
||||||
|
"@tiptap/extension-text-style": "^3.22.1",
|
||||||
|
"@tiptap/extension-typography": "^3.22.1",
|
||||||
|
"@tiptap/extensions": "^3.22.1",
|
||||||
|
"@tiptap/pm": "^3.22.1",
|
||||||
|
"@tiptap/react": "^3.22.1",
|
||||||
|
"@tiptap/starter-kit": "^3.22.1",
|
||||||
|
"@tiptap/suggestion": "^3.22.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"fuse.js": "^7.2.0",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
|
"lucide-react": "^1.7.0",
|
||||||
|
"motion": "^12.38.0",
|
||||||
|
"next": "16.2.2",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"react-fast-marquee": "^1.6.5",
|
||||||
|
"rough-notation": "^0.5.1",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.2.2",
|
||||||
|
"prettierrc": "^0.0.0-5",
|
||||||
|
"shadcn": "^4.1.2",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"ignoreScripts": [
|
||||||
|
"sharp",
|
||||||
|
"unrs-resolver"
|
||||||
|
],
|
||||||
|
"trustedDependencies": [
|
||||||
|
"sharp",
|
||||||
|
"unrs-resolver"
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+9230
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0 0 512 512" fill="none"><path fill="#FFFFFF" d="M0 412L0 100C0 44.7715 44.7715 0 100 0L412 0C467.228 0 512 44.7715 512 100L512 412C512 467.228 467.228 512 412 512L100 512C44.7715 512 0 467.228 0 412Z"></path><path fill="#000000" d="M74.0896 267.948L212.983 365.709C221.558 371.744 233.337 370.133 239.976 362.016L347.509 230.543C354.863 221.553 353.037 208.211 343.54 201.526L204.646 103.765C196.072 97.73 184.292 99.3415 177.653 107.458L70.1199 238.931C62.7666 247.921 64.5919 261.263 74.0896 267.948Z"></path><path fill="#000000" d="M438.649 267.728L299.756 365.488C291.181 371.524 279.402 369.912 272.763 361.796L165.23 230.323C157.876 221.332 159.702 207.99 169.199 201.305L308.093 103.545C316.667 97.5093 328.447 99.1209 335.086 107.237L442.619 238.71C449.972 247.701 448.147 261.043 438.649 267.728Z"></path><path fill="#2B2B2B" d="M127.238 306.279L241.71 420.841C249.521 428.659 262.193 428.659 270.005 420.841L384.477 306.279C392.28 298.47 392.28 285.815 384.477 278.006L270.005 163.444C262.193 155.627 249.521 155.627 241.71 163.444L127.238 278.006C119.435 285.815 119.435 298.47 127.238 306.279Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="438.488037109375" height="362.9998474121094" viewBox="0 0 438.488037109375 362.9998474121094" fill="none"><path fill="#FFFFFF" d="M36.9641 195.948L175.858 293.709C184.432 299.744 196.212 298.133 202.85 290.016L310.384 158.543C317.737 149.553 315.912 136.211 306.414 129.526L167.521 31.7651C158.946 25.73 147.166 27.3415 140.528 35.4578L32.9944 166.931C25.6411 175.921 27.4664 189.263 36.9641 195.948Z"></path><path fill="#FFFFFF" d="M401.524 195.728L262.63 293.488C254.056 299.524 242.276 297.912 235.638 289.796L128.104 158.323C120.751 149.332 122.576 135.99 132.074 129.305L270.967 31.5445C279.542 25.5093 291.322 27.1209 297.96 35.2372L405.494 166.71C412.847 175.701 411.022 189.043 401.524 195.728Z"></path><path fill="#F7F7F7" d="M90.1122 234.279L204.584 348.841C212.396 356.659 225.068 356.659 232.879 348.841L347.351 234.279C355.154 226.47 355.154 213.815 347.351 206.006L232.879 91.4445C225.068 83.6266 212.396 83.6266 204.584 91.4445L90.1122 206.006C82.3091 213.815 82.3091 226.47 90.1122 234.279Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="438.488037109375" height="362.9998474121094" viewBox="0 0 438.488037109375 362.9998474121094" fill="none"><path fill="#000000" d="M36.9641 195.948L175.858 293.709C184.432 299.744 196.212 298.133 202.85 290.016L310.384 158.543C317.737 149.553 315.912 136.211 306.414 129.526L167.521 31.7651C158.946 25.73 147.166 27.3415 140.528 35.4578L32.9944 166.931C25.6411 175.921 27.4664 189.263 36.9641 195.948Z"></path><path fill="#000000" d="M401.524 195.728L262.63 293.488C254.056 299.524 242.276 297.912 235.638 289.796L128.104 158.323C120.751 149.332 122.576 135.99 132.074 129.305L270.967 31.5445C279.542 25.5093 291.322 27.1209 297.96 35.2372L405.494 166.71C412.847 175.701 411.022 189.043 401.524 195.728Z"></path><path fill="#2B2B2B" d="M90.1122 234.279L204.584 348.841C212.396 356.659 225.068 356.659 232.879 348.841L347.351 234.279C355.154 226.47 355.154 213.815 347.351 206.006L232.879 91.4445C225.068 83.6266 212.396 83.6266 204.584 91.4445L90.1122 206.006C82.3091 213.815 82.3091 226.47 90.1122 234.279Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user