해결결
This commit is contained in:
parent
0f876c2cb6
commit
b185b9afb6
27 changed files with 1998 additions and 120 deletions
|
|
@ -1,48 +0,0 @@
|
|||
const events = [
|
||||
{ date: '2024-12-07', description: '??? ???? ?? ? ??', link: 'https://ncf.or.kr/' },
|
||||
{ date: '2024-12-07', description: '??? ???? ?? ?? ??', link: 'https://ncf.or.kr/' },
|
||||
{ date: '2024-08-18', description: '29회 해킹캠프 CTF 1위 (고민중독)', link: 'https://ctf.hackingcamp.org/' },
|
||||
{ date: '2024-08-05', description: '29회 해킹캠프 선발', link: 'https://hackingcamp.org/' },
|
||||
{ date: '2024-08-01', description: '글로벌 스타트업 학교 2기 베트남 해외 연수 데모데이 대상 (1위)', link: 'http://ncf.or.kr' },
|
||||
{ date: '2024-05-16', description: '글로벌 스타트업 학교 2기 합격', link: 'http://ncf.or.kr' },
|
||||
{ date: '2024-05-11', description: 'LG AI 청소년 캠프 1기 LG 탐색상 수상', link: 'https://lgaiyouthcamp.or.kr/' },
|
||||
{ date: '2024-05-11', description: 'LG AI 청소년 캠프 1기 수료', link: 'https://lgaiyouthcamp.or.kr/' },
|
||||
{ date: '2024-04-22', description: '@isangjeong.today (인천상정중학교의 오늘 급식)', link: 'https://www.instagram.com/isangjeong.today/' },
|
||||
{ date: '2024-04-06', description: 'TimeTable (Sekai 개조판 배포) [API 유실]', link: 'https://timeline.imnyang.xyz' },
|
||||
{ date: '2024-03-24', description: 'Dreamhack #133', link: 'https://dreamhack.io/users/40116/wargame' },
|
||||
{ date: '2024-03-24', description: 'Ubuntu Mirror', link: 'https://launchpad.net/ubuntu/+mirror/mirror.imnyang.xyz-release' },
|
||||
{ date: '2024-03-24', description: '내 목소리로 AI Cover 만들기', link: 'https://colab.research.google.com/drive/1a4G4hD9huBeGRZhEL2HNDMpqSuf4y61k?usp=sharing' },
|
||||
{ date: '2024-01-26', description: 'Fastapi를 통해 API 제작', link: 'https://github.com/imnyang/api' },
|
||||
{ date: '2023-12-20', description: 'LG AI 청소년 캠프 1기 합격' },
|
||||
{ date: '2023-11-14', description: '인천상정중학교 2023학년도 SW 문제 해결 활동 우수상(2위) 수여' },
|
||||
{ date: '2023-11-01', description: '블로그 시작', link: 'https://blog.imnyang.xyz' },
|
||||
{ date: '2023-10-12', description: '나는 로컬 시간을 알고 싶다', link: 'https://time.imnyang.xyz/' },
|
||||
{ date: '2023-09-24', description: 'sqlr.kr 기획 및 초기 개발', link: 'https://github.com/sqlare/sqlr.kr/tree/main' },
|
||||
{ date: '2023-09-02', description: '선린인터넷고등학교 제6회 소프트웨어나늠축제 Layer7 부서 과정 이수' },
|
||||
{ date: '2023-08-26', description: '컴시간 시간표를 더 나아보이게 Sekai', link: 'https://github.com/imnyang/Sekai' },
|
||||
{ date: '2023-08-23', description: '디스코드 통화방 녹음', link: 'https://github.com/imnyang/discord-voice-rec'},
|
||||
{ date: '2023-07-24', description: '한국정보기술연구원이 주도하는 사이버 가디언즈 보안캠프 수료' },
|
||||
{ date: '2023-03-20', description: '디스코드에서 대화형 인공지능 Siru 제작', link: 'https://github.com/imnyang/siru' },
|
||||
{ date: '2023-05-15', description: '한국 코드페어 예선 진출' },
|
||||
{ date: '2023-03-14', description: '타이머', link: 'https://github.com/imnyang/imnyang-timer' },
|
||||
{ date: '2022-12-20', description: '2022 SW영재 창작대회 은상 수상'},
|
||||
{ date: '2022-09-27', description: '2022 삼성 주니어 SW 창작대회 본선 진출' },
|
||||
{ date: '2022-05-23', description: '2022학년도 석정초SW영재학급 첫 수업' },
|
||||
{ date: '2022-07-26', description: '제 14회 맑은하늘 맑은웃음 공모전에서 맑은웃음상 수여' },
|
||||
{ date: '2021-11-14', description: 'Become a ZEPETO Creator 이수' },
|
||||
{ date: '2021-05-19', description: '소프트웨어와 전자신문이 주관한 소프트웨어재단 꿈찾기 캠프 이수' },
|
||||
{ date: '2018-01-27', description: '제4회 맑은하늘 맑은웃음 어린이 문예공모전에서 위닉스상(2위) 수여' },
|
||||
];
|
||||
|
||||
export default function Timeline() {
|
||||
return (
|
||||
<div>
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="flex flex-col gap-4">
|
||||
<p className="tabular-nums">{event.date}</p>
|
||||
{event.link ? <a href={event.link}>{event.description}</a> : event.description}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"use client"
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Repos() {
|
||||
const [userInfo, setUserInfo] = useState({ public_repos: 0, followers: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
const response = await fetch("https://api.github.com/users/imnyang");
|
||||
const data = await response.json();
|
||||
setUserInfo({ public_repos: data.public_repos, followers: data.followers });
|
||||
} catch (error) {
|
||||
console.error("Error fetching user info:", error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUserInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>{userInfo.public_repos}</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,17 +1,12 @@
|
|||
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
|
||||
@import url('https://unpkg.com/@catppuccin/palette/css/catppuccin.css');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--bg: var(--ctp-mocha-base);
|
||||
--fg: var(--ctp-mocha-crust);
|
||||
--primary: var(--ctp-mocha-lavender);
|
||||
--secondary: var(--ctp-mocha-mauve);
|
||||
--accent: var(--ctp-mocha-peach);
|
||||
--accent-foreground: var(--ctp-mocha-text);
|
||||
--bg: #101010;
|
||||
--fg: #fff;
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
import { Provider } from "@/components/ui/provider"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "imnyang",
|
||||
description: "imnyang's portfolio",
|
||||
|
|
@ -22,7 +24,7 @@ export default function RootLayout({
|
|||
<body
|
||||
className={`antialiased`}
|
||||
>
|
||||
{children}
|
||||
<Provider>{children}</Provider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState, forwardRef, Ref, Suspense } from "react";
|
||||
import React, { useEffect, useState, forwardRef, Ref, Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import "./index.css";
|
||||
|
||||
import { Link as LinkIcon } from 'lucide-react';
|
||||
import { Tooltip } from "@/components/ui/tooltip";
|
||||
|
||||
import { Icon, Stack } from "@chakra-ui/react"
|
||||
import {
|
||||
AccordionRoot,
|
||||
AccordionItem,
|
||||
AccordionItemContent,
|
||||
AccordionItemTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
import { Text } from "@chakra-ui/react"
|
||||
import { Heart, ChartBar } from "lucide-react";
|
||||
|
||||
const events = [
|
||||
{ date: '2024-12-07', description: '글로벌 스타트업 학교 팀 1위', link: 'https://blog.imnyang.xyz/blog/gss' },
|
||||
|
|
@ -124,8 +133,8 @@ export default function Home() {
|
|||
}
|
||||
|
||||
const SocialLink = ({ href, icon, tooltip }: SocialLinkProps) => (
|
||||
<Tippy content={tooltip} placement="bottom">
|
||||
<TippyWrapper
|
||||
<Tooltip content={tooltip} openDelay={100} positioning={{placement: "bottom"}}>
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
color: "#b2a1af",
|
||||
|
|
@ -136,10 +145,12 @@ export default function Home() {
|
|||
}}
|
||||
>
|
||||
<i className={icon} style={{ fontSize: "24px" }} />
|
||||
</TippyWrapper>
|
||||
</Tippy>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const [value, setValue] = useState(["about"])
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<div className="main">
|
||||
|
|
@ -156,23 +167,38 @@ export default function Home() {
|
|||
<SocialLink href="https://x.com/fur_local" icon="fa-brands fa-x-twitter" tooltip="X" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="timeline text-white">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="flex flex-col mb-3">
|
||||
<p className="tabular-nums">{event.date}</p>
|
||||
<div className="flex items-center">
|
||||
{event.link && (
|
||||
<Link href={event.link} className="flex gap-2">
|
||||
<LinkIcon width={18} />
|
||||
<span>{event.description}</span>
|
||||
</Link>
|
||||
)}
|
||||
{!event.link && <span className="ml-7">{event.description}</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Stack width="full" maxW="400px" mx="auto">
|
||||
<AccordionRoot multiple collapsible value={value} onValueChange={(e) => setValue(e.value)}>
|
||||
{items.map((item) => (
|
||||
<AccordionItem key={item.value} value={item.value}>
|
||||
<AccordionItemTrigger style={{ marginBottom: "0.5rem" }}>
|
||||
<Icon fontSize="lg" color="fg.subtle">
|
||||
{item.icon}
|
||||
</Icon>
|
||||
{item.title}
|
||||
</AccordionItemTrigger>
|
||||
<AccordionItemContent maxH="250px" overflow="auto">{item.content}</AccordionItemContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</AccordionRoot>
|
||||
</Stack>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const items = [
|
||||
{
|
||||
value: "about",
|
||||
icon: <Heart />,
|
||||
title: "About",
|
||||
content:
|
||||
"사람이래요.",
|
||||
},
|
||||
{
|
||||
value: "timeline",
|
||||
icon: <ChartBar />,
|
||||
title: "Timeline",
|
||||
content: <div className="timeline text-white">{events.map((event, index) => (<div key={index} className="flex flex-col gap-2 mb-3"><p className="tabular-nums text-base text-gray-400">{event.date}</p><div className="flex items-center">{event.link && (<Link href={event.link} className="flex gap-2 text-base"><span className="text-base">{event.description}</span><LinkIcon width={18} /></Link>)}{!event.link && <span className="text-base">{event.description}</span>}</div></div>))}</div>,
|
||||
},
|
||||
]
|
||||
|
|
@ -1,13 +1,6 @@
|
|||
import Link from 'next/link';
|
||||
import Timeline from '../components/Timeline';
|
||||
|
||||
export default function Timeline_Page() {
|
||||
return (
|
||||
<div style={{ display: 'flex', overflow: 'auto', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: 'white', width: '100vw', height: '100vh', background: '#101020' }}>
|
||||
<div style={{height: '70%', overflow: 'auto'}}>
|
||||
<Link href='/'>🏠 Back</Link>
|
||||
<Timeline />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
|
||||
export default async function TimelinePage( ) {
|
||||
redirect('/')
|
||||
}
|
||||
47
src/components/ui/accordion.tsx
Normal file
47
src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Accordion, HStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuChevronDown } from "react-icons/lu"
|
||||
|
||||
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
|
||||
indicatorPlacement?: "start" | "end"
|
||||
}
|
||||
|
||||
export const AccordionItemTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
AccordionItemTriggerProps
|
||||
>(function AccordionItemTrigger(props, ref) {
|
||||
const { children, indicatorPlacement = "end", ...rest } = props
|
||||
return (
|
||||
<Accordion.ItemTrigger {...rest} ref={ref}>
|
||||
{indicatorPlacement === "start" && (
|
||||
<Accordion.ItemIndicator rotate={{ base: "-90deg", _open: "0deg" }}>
|
||||
<LuChevronDown />
|
||||
</Accordion.ItemIndicator>
|
||||
)}
|
||||
<HStack gap="4" flex="1" textAlign="start" width="full">
|
||||
{children}
|
||||
</HStack>
|
||||
{indicatorPlacement === "end" && (
|
||||
<Accordion.ItemIndicator>
|
||||
<LuChevronDown />
|
||||
</Accordion.ItemIndicator>
|
||||
)}
|
||||
</Accordion.ItemTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
|
||||
|
||||
export const AccordionItemContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionItemContentProps
|
||||
>(function AccordionItemContent(props, ref) {
|
||||
return (
|
||||
<Accordion.ItemContent>
|
||||
<Accordion.ItemBody {...props} ref={ref} />
|
||||
</Accordion.ItemContent>
|
||||
)
|
||||
})
|
||||
|
||||
export const AccordionRoot = Accordion.Root
|
||||
export const AccordionItem = Accordion.Item
|
||||
74
src/components/ui/avatar.tsx
Normal file
74
src/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"use client"
|
||||
|
||||
import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react"
|
||||
import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
|
||||
|
||||
export interface AvatarProps extends ChakraAvatar.RootProps {
|
||||
name?: string
|
||||
src?: string
|
||||
srcSet?: string
|
||||
loading?: ImageProps["loading"]
|
||||
icon?: React.ReactElement
|
||||
fallback?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||
function Avatar(props, ref) {
|
||||
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||
props
|
||||
return (
|
||||
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||
<AvatarFallback name={name} icon={icon}>
|
||||
{fallback}
|
||||
</AvatarFallback>
|
||||
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||
{children}
|
||||
</ChakraAvatar.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface AvatarFallbackProps extends ChakraAvatar.FallbackProps {
|
||||
name?: string
|
||||
icon?: React.ReactElement
|
||||
}
|
||||
|
||||
const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
||||
function AvatarFallback(props, ref) {
|
||||
const { name, icon, children, ...rest } = props
|
||||
return (
|
||||
<ChakraAvatar.Fallback ref={ref} {...rest}>
|
||||
{children}
|
||||
{name != null && children == null && <>{getInitials(name)}</>}
|
||||
{name == null && children == null && (
|
||||
<ChakraAvatar.Icon asChild={!!icon}>{icon}</ChakraAvatar.Icon>
|
||||
)}
|
||||
</ChakraAvatar.Fallback>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function getInitials(name: string) {
|
||||
const names = name.trim().split(" ")
|
||||
const firstName = names[0] != null ? names[0] : ""
|
||||
const lastName = names.length > 1 ? names[names.length - 1] : ""
|
||||
return firstName && lastName
|
||||
? `${firstName.charAt(0)}${lastName.charAt(0)}`
|
||||
: firstName.charAt(0)
|
||||
}
|
||||
|
||||
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {}
|
||||
|
||||
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
||||
function AvatarGroup(props, ref) {
|
||||
const { size, variant, borderless, ...rest } = props
|
||||
return (
|
||||
<ChakraAvatar.PropsProvider value={{ size, variant, borderless }}>
|
||||
<Group gap="0" spaceX="-3" ref={ref} {...rest} />
|
||||
</ChakraAvatar.PropsProvider>
|
||||
)
|
||||
},
|
||||
)
|
||||
40
src/components/ui/button.tsx
Normal file
40
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react"
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
Button as ChakraButton,
|
||||
Span,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface ButtonLoadingProps {
|
||||
loading?: boolean
|
||||
loadingText?: React.ReactNode
|
||||
}
|
||||
|
||||
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function Button(props, ref) {
|
||||
const { loading, disabled, loadingText, children, ...rest } = props
|
||||
return (
|
||||
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
|
||||
{loading && !loadingText ? (
|
||||
<>
|
||||
<AbsoluteCenter display="inline-flex">
|
||||
<Spinner size="inherit" color="inherit" />
|
||||
</AbsoluteCenter>
|
||||
<Span opacity={0}>{children}</Span>
|
||||
</>
|
||||
) : loading && loadingText ? (
|
||||
<>
|
||||
<Spinner size="inherit" color="inherit" />
|
||||
{loadingText}
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ChakraButton>
|
||||
)
|
||||
},
|
||||
)
|
||||
25
src/components/ui/checkbox.tsx
Normal file
25
src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||
icon?: React.ReactNode
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
rootRef?: React.Ref<HTMLLabelElement>
|
||||
}
|
||||
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
function Checkbox(props, ref) {
|
||||
const { icon, children, inputProps, rootRef, ...rest } = props
|
||||
return (
|
||||
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraCheckbox.Control>
|
||||
{icon || <ChakraCheckbox.Indicator />}
|
||||
</ChakraCheckbox.Control>
|
||||
{children != null && (
|
||||
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||
)}
|
||||
</ChakraCheckbox.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
17
src/components/ui/close-button.tsx
Normal file
17
src/components/ui/close-button.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { ButtonProps } from "@chakra-ui/react"
|
||||
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuX } from "react-icons/lu"
|
||||
|
||||
export type CloseButtonProps = ButtonProps
|
||||
|
||||
export const CloseButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
CloseButtonProps
|
||||
>(function CloseButton(props, ref) {
|
||||
return (
|
||||
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||
{props.children ?? <LuX />}
|
||||
</ChakraIconButton>
|
||||
)
|
||||
})
|
||||
67
src/components/ui/color-mode.tsx
Normal file
67
src/components/ui/color-mode.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"use client"
|
||||
|
||||
import type { IconButtonProps } from "@chakra-ui/react"
|
||||
import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react"
|
||||
import { ThemeProvider, useTheme } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { LuMoon, LuSun } from "react-icons/lu"
|
||||
|
||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||
|
||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export function useColorMode() {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === "light" ? "dark" : "light")
|
||||
}
|
||||
return {
|
||||
colorMode: resolvedTheme,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
}
|
||||
|
||||
export function useColorModeValue<T>(light: T, dark: T) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "light" ? light : dark
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "light" ? <LuSun /> : <LuMoon />
|
||||
}
|
||||
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||
|
||||
export const ColorModeButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ColorModeButtonProps
|
||||
>(function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
aria-label="Toggle color mode"
|
||||
size="sm"
|
||||
ref={ref}
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: "5",
|
||||
height: "5",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
})
|
||||
62
src/components/ui/dialog.tsx
Normal file
62
src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface DialogContentProps extends ChakraDialog.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
backdrop?: boolean
|
||||
}
|
||||
|
||||
export const DialogContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DialogContentProps
|
||||
>(function DialogContent(props, ref) {
|
||||
const {
|
||||
children,
|
||||
portalled = true,
|
||||
portalRef,
|
||||
backdrop = true,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
{backdrop && <ChakraDialog.Backdrop />}
|
||||
<ChakraDialog.Positioner>
|
||||
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDialog.Content>
|
||||
</ChakraDialog.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const DialogCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraDialog.CloseTriggerProps
|
||||
>(function DialogCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraDialog.CloseTrigger
|
||||
position="absolute"
|
||||
top="2"
|
||||
insetEnd="2"
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<CloseButton size="sm" ref={ref}>
|
||||
{props.children}
|
||||
</CloseButton>
|
||||
</ChakraDialog.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const DialogRoot = ChakraDialog.Root
|
||||
export const DialogFooter = ChakraDialog.Footer
|
||||
export const DialogHeader = ChakraDialog.Header
|
||||
export const DialogBody = ChakraDialog.Body
|
||||
export const DialogBackdrop = ChakraDialog.Backdrop
|
||||
export const DialogTitle = ChakraDialog.Title
|
||||
export const DialogDescription = ChakraDialog.Description
|
||||
export const DialogTrigger = ChakraDialog.Trigger
|
||||
export const DialogActionTrigger = ChakraDialog.ActionTrigger
|
||||
52
src/components/ui/drawer.tsx
Normal file
52
src/components/ui/drawer.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
offset?: ChakraDrawer.ContentProps["padding"]
|
||||
}
|
||||
|
||||
export const DrawerContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DrawerContentProps
|
||||
>(function DrawerContent(props, ref) {
|
||||
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraDrawer.Positioner padding={offset}>
|
||||
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDrawer.Content>
|
||||
</ChakraDrawer.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const DrawerCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraDrawer.CloseTriggerProps
|
||||
>(function DrawerCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraDrawer.CloseTrigger
|
||||
position="absolute"
|
||||
top="2"
|
||||
insetEnd="2"
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<CloseButton size="sm" ref={ref} />
|
||||
</ChakraDrawer.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const DrawerTrigger = ChakraDrawer.Trigger
|
||||
export const DrawerRoot = ChakraDrawer.Root
|
||||
export const DrawerFooter = ChakraDrawer.Footer
|
||||
export const DrawerHeader = ChakraDrawer.Header
|
||||
export const DrawerBody = ChakraDrawer.Body
|
||||
export const DrawerBackdrop = ChakraDrawer.Backdrop
|
||||
export const DrawerDescription = ChakraDrawer.Description
|
||||
export const DrawerTitle = ChakraDrawer.Title
|
||||
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
|
||||
33
src/components/ui/field.tsx
Normal file
33
src/components/ui/field.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Field as ChakraField } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
errorText?: React.ReactNode
|
||||
optionalText?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||
function Field(props, ref) {
|
||||
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||
props
|
||||
return (
|
||||
<ChakraField.Root ref={ref} {...rest}>
|
||||
{label && (
|
||||
<ChakraField.Label>
|
||||
{label}
|
||||
<ChakraField.RequiredIndicator fallback={optionalText} />
|
||||
</ChakraField.Label>
|
||||
)}
|
||||
{children}
|
||||
{helperText && (
|
||||
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
|
||||
)}
|
||||
{errorText && (
|
||||
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||
)}
|
||||
</ChakraField.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
53
src/components/ui/input-group.tsx
Normal file
53
src/components/ui/input-group.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
|
||||
import { Group, InputElement } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface InputGroupProps extends BoxProps {
|
||||
startElementProps?: InputElementProps
|
||||
endElementProps?: InputElementProps
|
||||
startElement?: React.ReactNode
|
||||
endElement?: React.ReactNode
|
||||
children: React.ReactElement
|
||||
startOffset?: InputElementProps["paddingStart"]
|
||||
endOffset?: InputElementProps["paddingEnd"]
|
||||
}
|
||||
|
||||
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||
function InputGroup(props, ref) {
|
||||
const {
|
||||
startElement,
|
||||
startElementProps,
|
||||
endElement,
|
||||
endElementProps,
|
||||
children,
|
||||
startOffset = "6px",
|
||||
endOffset = "6px",
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const child =
|
||||
React.Children.only<React.ReactElement<InputElementProps>>(children)
|
||||
|
||||
return (
|
||||
<Group ref={ref} {...rest}>
|
||||
{startElement && (
|
||||
<InputElement pointerEvents="none" {...startElementProps}>
|
||||
{startElement}
|
||||
</InputElement>
|
||||
)}
|
||||
{React.cloneElement(child, {
|
||||
...(startElement && {
|
||||
ps: `calc(var(--input-height) - ${startOffset})`,
|
||||
}),
|
||||
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
|
||||
...children.props,
|
||||
})}
|
||||
{endElement && (
|
||||
<InputElement placement="end" {...endElementProps}>
|
||||
{endElement}
|
||||
</InputElement>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
)
|
||||
59
src/components/ui/popover.tsx
Normal file
59
src/components/ui/popover.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface PopoverContentProps extends ChakraPopover.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const PopoverContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PopoverContentProps
|
||||
>(function PopoverContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraPopover.Positioner>
|
||||
<ChakraPopover.Content ref={ref} {...rest} />
|
||||
</ChakraPopover.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraPopover.ArrowProps
|
||||
>(function PopoverArrow(props, ref) {
|
||||
return (
|
||||
<ChakraPopover.Arrow {...props} ref={ref}>
|
||||
<ChakraPopover.ArrowTip />
|
||||
</ChakraPopover.Arrow>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraPopover.CloseTriggerProps
|
||||
>(function PopoverCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraPopover.CloseTrigger
|
||||
position="absolute"
|
||||
top="1"
|
||||
insetEnd="1"
|
||||
{...props}
|
||||
asChild
|
||||
ref={ref}
|
||||
>
|
||||
<CloseButton size="sm" />
|
||||
</ChakraPopover.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverTitle = ChakraPopover.Title
|
||||
export const PopoverDescription = ChakraPopover.Description
|
||||
export const PopoverFooter = ChakraPopover.Footer
|
||||
export const PopoverHeader = ChakraPopover.Header
|
||||
export const PopoverRoot = ChakraPopover.Root
|
||||
export const PopoverBody = ChakraPopover.Body
|
||||
export const PopoverTrigger = ChakraPopover.Trigger
|
||||
15
src/components/ui/provider.tsx
Normal file
15
src/components/ui/provider.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"use client"
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode"
|
||||
|
||||
export function Provider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
24
src/components/ui/radio.tsx
Normal file
24
src/components/ui/radio.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface RadioProps extends ChakraRadioGroup.ItemProps {
|
||||
rootRef?: React.Ref<HTMLDivElement>
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||
function Radio(props, ref) {
|
||||
const { children, inputProps, rootRef, ...rest } = props
|
||||
return (
|
||||
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
|
||||
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraRadioGroup.ItemIndicator />
|
||||
{children && (
|
||||
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
|
||||
)}
|
||||
</ChakraRadioGroup.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const RadioGroup = ChakraRadioGroup.Root
|
||||
82
src/components/ui/slider.tsx
Normal file
82
src/components/ui/slider.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface SliderProps extends ChakraSlider.RootProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
label?: React.ReactNode
|
||||
showValue?: boolean
|
||||
}
|
||||
|
||||
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||
function Slider(props, ref) {
|
||||
const { marks: marksProp, label, showValue, ...rest } = props
|
||||
const value = props.defaultValue ?? props.value
|
||||
|
||||
const marks = marksProp?.map((mark) => {
|
||||
if (typeof mark === "number") return { value: mark, label: undefined }
|
||||
return mark
|
||||
})
|
||||
|
||||
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
||||
|
||||
return (
|
||||
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
||||
{label && !showValue && (
|
||||
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
||||
)}
|
||||
{label && showValue && (
|
||||
<HStack justify="space-between">
|
||||
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
||||
<ChakraSlider.ValueText />
|
||||
</HStack>
|
||||
)}
|
||||
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
|
||||
<ChakraSlider.Track>
|
||||
<ChakraSlider.Range />
|
||||
</ChakraSlider.Track>
|
||||
<SliderThumbs value={value} />
|
||||
<SliderMarks marks={marks} />
|
||||
</ChakraSlider.Control>
|
||||
</ChakraSlider.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function SliderThumbs(props: { value?: number[] }) {
|
||||
const { value } = props
|
||||
return (
|
||||
<For each={value}>
|
||||
{(_, index) => (
|
||||
<ChakraSlider.Thumb key={index} index={index}>
|
||||
<ChakraSlider.HiddenInput />
|
||||
</ChakraSlider.Thumb>
|
||||
)}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
|
||||
interface SliderMarksProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
}
|
||||
|
||||
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
|
||||
function SliderMarks(props, ref) {
|
||||
const { marks } = props
|
||||
if (!marks?.length) return null
|
||||
|
||||
return (
|
||||
<ChakraSlider.MarkerGroup ref={ref}>
|
||||
{marks.map((mark, index) => {
|
||||
const value = typeof mark === "number" ? mark : mark.value
|
||||
const label = typeof mark === "number" ? undefined : mark.label
|
||||
return (
|
||||
<ChakraSlider.Marker key={index} value={value}>
|
||||
<ChakraSlider.MarkerIndicator />
|
||||
{label}
|
||||
</ChakraSlider.Marker>
|
||||
)
|
||||
})}
|
||||
</ChakraSlider.MarkerGroup>
|
||||
)
|
||||
},
|
||||
)
|
||||
46
src/components/ui/tooltip.tsx
Normal file
46
src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
content: React.ReactNode
|
||||
contentProps?: ChakraTooltip.ContentProps
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
function Tooltip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
disabled,
|
||||
portalled,
|
||||
content,
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (disabled) return children
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraTooltip.Positioner>
|
||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||
{showArrow && (
|
||||
<ChakraTooltip.Arrow>
|
||||
<ChakraTooltip.ArrowTip />
|
||||
</ChakraTooltip.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraTooltip.Content>
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue