first commit

This commit is contained in:
dekun
2026-06-10 19:59:27 +08:00
commit d141623070
813 changed files with 61301 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import Image from "next/image";
const rotationDuration = 3800;
const bezier = "cubic-bezier(0.645,0.045,0.355,1)";
function Coin(props: {
frontList: boolean[];
rotation: boolean;
onTransitionEnd: any;
}) {
const [lastFront, setLastFront] = useState(props.frontList);
useEffect(function () {
if (!props.rotation) {
return;
}
let id = setTimeout(function () {
setLastFront(props.frontList);
props.onTransitionEnd();
}, rotationDuration);
return () => clearTimeout(id);
});
return (
<div className="flex w-full max-w-md justify-around rounded-md border bg-secondary p-4 shadow dark:border-0 dark:shadow-none sm:p-6">
{props.frontList.map((value, index) => (
<CoinItem
key={index}
front={value}
lastFront={lastFront[index]}
rotation={props.rotation}
/>
))}
</div>
);
}
function CoinItem(props: {
front: boolean;
lastFront: boolean;
rotation: boolean;
onTransitionEnd?: any;
}) {
let animate = "";
if (props.rotation) {
// animate-[coin-front-front_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
// animate-[coin-front-back_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
// animate-[coin-back-front_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
// animate-[coin-back-back_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
animate = `animate-[coin-${getFront(props.lastFront)}-${getFront(
props.front,
)}_${rotationDuration / 1000}s_${bezier}]`;
}
return (
<div
style={{
transform: `rotateY(${props.front ? 0 : 180}deg)`,
transformStyle: "preserve-3d",
transformOrigin: "50% 50% -0.5px",
}}
className={clsx("h-16 w-16 sm:h-20 sm:w-20", animate)}
>
<Image
width={0}
height={0}
sizes="100vw"
draggable={false}
className="absolute w-full"
src="/img/head.webp"
alt="coin"
/>
<Image
width={0}
height={0}
sizes="100vw"
draggable={false}
className="absolute h-full w-full"
style={{ transform: "translateZ(-1px)" }}
src="/img/tail.webp"
alt="coin"
/>
</div>
);
}
function getFront(front: boolean): string {
return front ? "front" : "back";
}
export default Coin;
+256
View File
@@ -0,0 +1,256 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import Coin from "@/components/coin";
import Hexagram, { HexagramObj } from "@/components/hexagram";
import { bool } from "aimless.js";
import Result, { ResultObj } from "@/components/result";
import Question from "@/components/question";
import ResultAI from "@/components/result-ai";
import { animateChildren } from "@/lib/animate";
import guaIndexData from "@/lib/data/gua-index.json";
import guaListData from "@/lib/data/gua-list.json";
import { getAnswer } from "@/app/server";
import { readStreamableValue } from "ai/rsc";
import { Button } from "./ui/button";
import { BrainCircuit, ListRestart } from "lucide-react";
import { ERROR_PREFIX } from "@/lib/constant";
const AUTO_DELAY = 600;
function Divination() {
const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [completion, setCompletion] = useState<string>("");
async function onCompletion() {
setError("");
setCompletion("");
setIsLoading(true);
try {
const { data, error } = await getAnswer(
question,
resultObj!.guaMark,
resultObj!.guaTitle,
resultObj!.guaResult,
resultObj!.guaChange,
);
if (error) {
setError(error);
return;
}
if (data) {
let ret = "";
for await (const delta of readStreamableValue(data)) {
if (delta.startsWith(ERROR_PREFIX)) {
setError(delta.slice(ERROR_PREFIX.length));
return;
}
ret += delta;
setCompletion(ret);
}
}
} catch (err: any) {
setError(err.message ?? err);
} finally {
setIsLoading(false);
}
}
const [frontList, setFrontList] = useState([true, true, true]);
const [rotation, setRotation] = useState(false);
const [hexagramList, setHexagramList] = useState<HexagramObj[]>([]);
const [resultObj, setResultObj] = useState<ResultObj | null>(null);
const [question, setQuestion] = useState("");
const [resultAi, setResultAi] = useState(false);
const flexRef = useRef<HTMLDivElement>(null);
const [count, setCount] = useState(0);
// 自动卜筮
useEffect(() => {
if (rotation || resultObj || count >= 6 || !question) {
return;
}
setTimeout(startClick, AUTO_DELAY);
}, [question, rotation]);
useEffect(() => {
if (!flexRef.current) {
return;
}
const observer = animateChildren(flexRef.current);
return () => observer.disconnect();
}, []);
function onTransitionEnd() {
setRotation(false);
let frontCount = frontList.reduce((acc, val) => (val ? acc + 1 : acc), 0);
setHexagramList((list) => {
const newList = [
...list,
{
change: frontCount == 0 || frontCount == 3 || null,
yang: frontCount >= 2,
separate: list.length == 3,
},
];
setResult(newList);
return newList;
});
}
function startClick() {
if (rotation) {
return;
}
if (hexagramList.length >= 6) {
setHexagramList([]);
}
setFrontList([bool(), bool(), bool()]);
setRotation(true);
setCount(count + 1);
}
async function testClick() {
for (let i = 0; i < 6; i++) {
onTransitionEnd();
}
}
function restartClick() {
setResultObj(null);
setHexagramList([]);
setQuestion("");
setResultAi(false);
setCount(0);
stop();
}
function aiClick() {
setResultAi(true);
onCompletion();
}
function setResult(list: HexagramObj[]) {
if (list.length != 6) {
return;
}
const guaDict1 = ["坤", "震", "坎", "兑", "艮", "离", "巽", "乾"];
const guaDict2 = ["地", "雷", "水", "泽", "山", "火", "风", "天"];
const changeYang = ["初九", "九二", "九三", "九四", "九五", "上九"];
const changeYin = ["初六", "六二", "六三", "六四", "六五", "上六"];
const changeList: String[] = [];
list.forEach((value, index) => {
if (!value.change) {
return;
}
changeList.push(value.yang ? changeYang[index] : changeYin[index]);
});
// 卦的结果: 第X卦 X卦 XX卦 X上X下
// 计算卦的索引,111对应乾卦,000对应坤卦,索引转为10进制。
const upIndex =
(list[5].yang ? 4 : 0) + (list[4].yang ? 2 : 0) + (list[3].yang ? 1 : 0);
const downIndex =
(list[2].yang ? 4 : 0) + (list[1].yang ? 2 : 0) + (list[0].yang ? 1 : 0);
const guaIndex = guaIndexData[upIndex][downIndex] - 1;
const guaName1 = guaListData[guaIndex];
let guaName2;
if (upIndex === downIndex) {
// 上下卦相同,格式为X为X
guaName2 = guaDict1[upIndex] + "为" + guaDict2[upIndex];
} else {
guaName2 = guaDict2[upIndex] + guaDict2[downIndex] + guaName1;
}
const guaDesc = guaDict1[upIndex] + "上" + guaDict1[downIndex] + "下";
setResultObj({
// 例:26.山天大畜
guaMark: `${(guaIndex + 1).toString().padStart(2, "0")}.${guaName2}`,
guaTitle: `周易第${guaIndex + 1}`,
// 例:大畜卦(山天大畜)_艮上乾下
guaResult: `${guaName1}卦(${guaName2})_${guaDesc}`,
guaChange:
changeList.length === 0 ? "无变爻" : `变爻: ${changeList.toString()}`,
});
}
const showResult = resultObj !== null;
const inputQuestion = question === "";
return (
<main
ref={flexRef}
className="gap mx-auto flex h-0 w-[90%] flex-1 flex-col flex-nowrap items-center"
>
<Question question={question} setQuestion={setQuestion} />
{!resultAi && !inputQuestion && (
<Coin
onTransitionEnd={onTransitionEnd}
frontList={frontList}
rotation={rotation}
/>
)}
{!inputQuestion && !showResult && (
<div className="relative">
<span className="pl-2 text-lg font-medium">
🎲 {" "}
<span className="font-mono text-xl font-bold text-orange-500">
{count === 0 ? "-/-" : `${count}/6`}
</span>{" "}
</span>
</div>
)}
{!inputQuestion && hexagramList.length != 0 && (
<div className="flex max-w-md gap-2">
<Hexagram list={hexagramList} />
{showResult && (
<div className="flex flex-col justify-around">
<Result {...resultObj} />
<div className="flex flex-col gap-2 sm:px-6">
<Button
size="sm"
variant="destructive"
onClick={restartClick}
disabled={rotation}
>
<ListRestart size={18} className="mr-1" />
</Button>
{resultAi ? null : (
<Button size="sm" onClick={aiClick} disabled={rotation}>
<BrainCircuit size={16} className="mr-1" />
AI
</Button>
)}
</div>
</div>
)}
</div>
)}
{resultAi && (
<ResultAI
completion={completion}
isLoading={isLoading}
onCompletion={onCompletion}
error={error}
/>
)}
</main>
);
}
export default Divination;
+14
View File
@@ -0,0 +1,14 @@
import React from "react";
import { VERSION } from "@/lib/constant";
function Footer() {
return (
<footer className="mx-auto flex items-center gap-1 text-xs text-muted-foreground/80">
<span className="italic"></span>
<span className="text-muted-foreground/60">·</span>
<span> v{VERSION}</span>
</footer>
);
}
export default Footer;
+16
View File
@@ -0,0 +1,16 @@
import { ChatGPT } from "@/components/svg";
import { ModeToggle } from "@/components/mode-toggle";
export default function Header() {
return (
<header className="h-14 bg-secondary py-2 shadow">
<div className="mx-auto flex h-full w-full items-center justify-center sm:max-w-md sm:justify-between md:max-w-2xl">
<div className="flex gap-2">
<ChatGPT />
<span></span>
</div>
<ModeToggle />
</div>
</header>
);
}
+59
View File
@@ -0,0 +1,59 @@
import React from "react";
import clsx from "clsx";
export interface HexagramObj {
change: boolean | null;
yang: boolean;
separate: boolean;
}
function Hexagram(props: { list: HexagramObj[] }) {
return (
<div className="flex h-52 w-56 shrink-0 flex-col-reverse gap-1.5 overflow-hidden rounded-md border bg-secondary py-3 shadow-inner dark:border-0 dark:shadow-none sm:h-60 sm:w-72">
{props.list.map((value, index) => {
return (
<div key={index} className="flex flex-col-reverse gap-1.5">
{value.separate && <div className="h-0.5 sm:h-1" />}
<Line change={value.change} yang={value.yang} />
</div>
);
})}
</div>
);
}
function Line(props: { change: boolean | null; yang: boolean }) {
let changeYang = props.change && props.yang;
const color = props.change ? "bg-red-400" : "bg-stone-400";
return (
<div className="flex h-[24px] w-full animate-[transform-x_0.3s_ease-out] items-center justify-center sm:h-[29px]">
{props.yang ? (
<div className={clsx("h-full w-4/5 sm:w-[83%]", color)}></div>
) : (
<div className="flex h-full w-4/5 justify-between sm:w-[83%]">
<div className={clsx("h-full w-[46%]", color)}></div>
<div className={clsx("h-full w-[46%]", color)}></div>
</div>
)}
{props.change ? <Change changeYang={changeYang} /> : null}
</div>
);
}
function Change(props: { changeYang: boolean | null }) {
return (
<div className="h-0 w-0">
<div className="relative -right-1 -top-3">
{props.changeYang ? (
<span className="text-sm text-muted-foreground"></span>
) : (
<span className="relative -right-0.5 text-sm text-muted-foreground">
</span>
)}
</div>
</div>
);
}
export default Hexagram;
+40
View File
@@ -0,0 +1,40 @@
"use client";
import * as React from "react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Laptop2, MoonStar, Sun } from "lucide-react";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="hidden sm:block">
<Sun size={22} strokeWidth={1.5} className="dark:hidden" />
<MoonStar size={22} strokeWidth={1.5} className="hidden dark:block" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun size={20} strokeWidth={1.5} className="mr-2" />
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<MoonStar size={20} strokeWidth={1.5} className="mr-2" />
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Laptop2 size={20} strokeWidth={1.5} className="mr-2" /> System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+84
View File
@@ -0,0 +1,84 @@
import React, { createRef } from "react";
import clsx from "clsx";
import todayJson from "@/lib/data/today.json";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import Image from "next/image";
const todayData: string[] = todayJson;
function Question(props: { question: string; setQuestion: any }) {
const inputRef = createRef<HTMLTextAreaElement>();
function startClick() {
const value = inputRef.current?.value;
if (value === "") {
return;
}
props.setQuestion(value);
}
function todayClick(index: number) {
props.setQuestion(todayData[index]);
}
return (
<div
className={clsx(
"ignore-animate flex w-full max-w-md flex-col gap-4",
props.question || "pt-6",
)}
>
{props.question === "" ? (
<>
<label></label>
<Textarea
ref={inputRef}
placeholder="将使用 AI 为您解读"
className="resize-none"
rows={4}
/>
<div className="flex flex-row-reverse">
<Button size="sm" onClick={startClick}>
</Button>
</div>
<label className="mt-16 underline underline-offset-4">
🧐 西
</label>
<div className="flex flex-wrap gap-3">
{todayData.map(function (value, index) {
return (
<span
key={index}
onClick={() => {
todayClick(index);
}}
className="rounded-md border bg-secondary p-2 text-sm text-muted-foreground shadow transition hover:scale-[1.03] dark:border-0 dark:text-foreground/80 dark:shadow-none"
>
{value}
</span>
);
})}
</div>
</>
) : null}
{props.question && (
<div className="flex truncate rounded-md border bg-secondary p-2 shadow dark:border-0 dark:shadow-none">
<Image
width={24}
height={24}
className="mr-2"
src="/img/yin-yang.webp"
alt="yinyang"
/>
{props.question}
</div>
)}
</div>
);
}
export default Question;
+91
View File
@@ -0,0 +1,91 @@
import React, { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { RotateCw } from "lucide-react";
import Markdown from "react-markdown";
function ResultAI({
completion,
isLoading,
onCompletion,
error,
}: {
completion: string;
isLoading: boolean;
onCompletion: () => void;
error: string;
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(false);
useEffect(() => {
setAutoScroll(isLoading);
}, [isLoading]);
useEffect(() => {
if (!autoScroll) {
return;
}
scrollTo();
});
function scrollTo() {
requestAnimationFrame(() => {
if (
!scrollRef.current ||
scrollRef.current.scrollHeight ===
scrollRef.current.clientHeight + scrollRef.current.scrollTop
) {
return;
}
scrollRef.current.scrollTo(0, scrollRef.current.scrollHeight);
});
}
function onScroll(e: HTMLElement) {
if (!isLoading) {
return;
}
const hitBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 15;
if (hitBottom === autoScroll) {
return;
}
setAutoScroll(hitBottom);
}
return (
<div className="h-0 w-full flex-1 sm:max-w-md md:max-w-2xl">
{isLoading && (
<div className="flex h-0">
<span className="flex-1" />
<div className="relative -top-4 flex w-fit items-center pr-1 text-muted-foreground sm:left-2 sm:pr-3">
<RotateCw size={16} className="animate-spin" />
<span className="ml-1 text-sm">AI ...</span>
</div>
</div>
)}
<div
ref={scrollRef}
onScroll={(e) => onScroll(e.currentTarget)}
className="max-h-full overflow-auto rounded-md border p-3 shadow dark:border-0 dark:bg-secondary/90 dark:shadow-none sm:p-5"
>
{error ? (
<div className="text-destructive">
_ಠ
<br />
{error}
</div>
) : (
<Markdown className="prose dark:prose-invert">{completion}</Markdown>
)}
{!isLoading && (
<Button onClick={onCompletion} size="sm" className="mt-2">
<RotateCw size={18} className="mr-1" />
</Button>
)}
</div>
</div>
);
}
export default ResultAI;
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
export interface ResultObj {
guaTitle: string;
guaMark: string;
guaResult: string;
guaChange: string;
}
function Result(props: ResultObj) {
return (
<div className="flex flex-col items-start justify-center gap-2 sm:gap-3">
{props.guaTitle}
<a
className="group flex items-center gap-1 font-medium text-primary/80 underline underline-offset-4 transition-colors hover:text-primary/100"
href={`/learn/${props.guaMark}`}
>
<div className="mt-1 h-[90%] w-1.5 bg-blue-400/80 transition-colors group-hover:bg-blue-400/100" />
<span>{props.guaResult}</span>
</a>
<span className="text-sm italic text-muted-foreground">
{props.guaChange}
</span>
</div>
);
}
export default Result;
+33
View File
@@ -0,0 +1,33 @@
import React from "react";
export const ChatGPT = React.memo(function ChatGPT({
className = "w-6 h-6",
strokeWidth = 3,
}: {
className?: string;
strokeWidth?: number;
}) {
return (
<svg
width="1em"
height="1em"
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinejoin="round"
>
<path d="M18.38 27.94v-14.4l11.19-6.46c6.2-3.58 17.3 5.25 12.64 13.33"></path>
<path d="m18.38 20.94l12.47-7.2l11.19 6.46c6.2 3.58 4.1 17.61-5.23 17.61"></path>
<path d="m24.44 17.44l12.47 7.2v12.93c0 7.16-13.2 12.36-17.86 4.28"></path>
<path d="M30.5 21.2v14.14L19.31 41.8c-6.2 3.58-17.3-5.25-12.64-13.33"></path>
<path d="m30.5 27.94l-12.47 7.2l-11.19-6.46c-6.21-3.59-4.11-17.61 5.22-17.61"></path>
<path d="m24.44 31.44l-12.47-7.2V11.31c0-7.16 13.2-12.36 17.86-4.28"></path>
</g>
</svg>
);
});
+56
View File
@@ -0,0 +1,56 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+201
View File
@@ -0,0 +1,201 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
onCloseAutoFocus={(e) => e.preventDefault()}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";
export { Textarea };
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
import Script from "next/script";
import { unstable_noStore as noStore } from "next/cache";
function Umami() {
noStore();
if (!process.env.UMAMI_ID) {
return;
}
return (
<Script
id="umami"
strategy="afterInteractive"
src={process.env.UMAMI_URL}
data-website-id={process.env.UMAMI_ID}
data-domains={process.env.UMAMI_DOMAINS}
/>
);
}
export default Umami;