React router #3

Merged
imnyang merged 2 commits from react-router into main 2024-12-31 12:35:34 +09:00
48 changed files with 683 additions and 5917 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md

View file

@ -1,7 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}

42
.gitignore vendored
View file

@ -1,40 +1,6 @@
# 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 .DS_Store
*.pem /node_modules/
# debug # React Router
npm-debug.log* /.react-router/
yarn-debug.log* /build/
yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci
FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev
FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build
FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]

25
Dockerfile.bun Normal file
View file

@ -0,0 +1,25 @@
FROM oven/bun:1 AS dependencies-env
COPY . /app
FROM dependencies-env AS development-dependencies-env
COPY ./package.json bun.lockb /app/
WORKDIR /app
RUN bun i --frozen-lockfile
FROM dependencies-env AS production-dependencies-env
COPY ./package.json bun.lockb /app/
WORKDIR /app
RUN bun i --production
FROM dependencies-env AS build-env
COPY ./package.json bun.lockb /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN bun run build
FROM dependencies-env
COPY ./package.json bun.lockb /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["bun", "run", "start"]

26
Dockerfile.pnpm Normal file
View file

@ -0,0 +1,26 @@
FROM node:20-alpine AS dependencies-env
RUN npm i -g pnpm
COPY . /app
FROM dependencies-env AS development-dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
WORKDIR /app
RUN pnpm i --frozen-lockfile
FROM dependencies-env AS production-dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
WORKDIR /app
RUN pnpm i --prod --frozen-lockfile
FROM dependencies-env AS build-env
COPY ./package.json pnpm-lock.yaml /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN pnpm build
FROM dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["pnpm", "start"]

101
README.md
View file

@ -1 +1,100 @@
# A # Welcome to React Router!
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
## Features
- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)
## Getting Started
### Installation
Install the dependencies:
```bash
npm install
```
### Development
Start the development server with HMR:
```bash
npm run dev
```
Your application will be available at `http://localhost:5173`.
## Building for Production
Create a production build:
```bash
npm run build
```
## Deployment
### Docker Deployment
This template includes three Dockerfiles optimized for different package managers:
- `Dockerfile` - for npm
- `Dockerfile.pnpm` - for pnpm
- `Dockerfile.bun` - for bun
To build and run using Docker:
```bash
# For npm
docker build -t my-app .
# For pnpm
docker build -f Dockerfile.pnpm -t my-app .
# For bun
docker build -f Dockerfile.bun -t my-app .
# Run the container
docker run -p 3000:3000 my-app
```
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### DIY Deployment
If you're familiar with deploying Node applications, the built-in app server is production-ready.
Make sure to deploy the output of `npm run build`
```
├── package.json
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```
## Styling
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
---
Built with ❤️ using React Router.

9
app/app.css Normal file
View file

@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body {
background-color: #0a0a0a;
color-scheme: dark;
height: 100%;
}

View file

@ -0,0 +1,71 @@
import { useEffect, useRef } from "react";
const TABS = [
{ label: "Home" },
{ label: "Timeline" }
];
interface AnimatedTabsProps {
activeTab: string;
setActiveTab: (tab: string) => void;
}
export function AnimatedTabs({ activeTab, setActiveTab }: AnimatedTabsProps) {
const containerRef = useRef<HTMLDivElement>(null);
const activeTabRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const container = containerRef.current;
if (container && activeTab) {
const activeTabElement = activeTabRef.current;
if (activeTabElement) {
const { offsetLeft, offsetWidth } = activeTabElement;
const clipLeft = offsetLeft;
const clipRight = offsetLeft + offsetWidth;
container.style.clipPath = `inset(0 ${Number(100 - (clipRight / container.offsetWidth) * 100).toFixed()}% 0 ${Number((clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`;
}
}
}, [activeTab, activeTabRef, containerRef]);
return (
<div className="relative mx-auto flex w-fit flex-col items-center rounded-full">
<div
ref={containerRef}
className="absolute z-10 w-full overflow-hidden [clip-path:inset(0px_75%_0px_0%_round_17px)] [transition:clip-path_0.25s_ease]"
>
<div className="relative flex w-full justify-center bg-white">
{TABS.map((tab, index) => (
<button
key={index}
onClick={() => setActiveTab(tab.label)}
className="flex h-8 items-center rounded-full p-3 text-sm font-medium text-black"
tabIndex={-1}
>
{tab.label}
</button>
))}
</div>
</div>
<div className="relative flex w-full justify-center">
{TABS.map(({ label }, index) => {
const isActive = activeTab === label;
return (
<button
key={index}
ref={isActive ? activeTabRef : null}
onClick={() => setActiveTab(label)}
className="flex h-8 items-center rounded-full p-3 text-sm font-medium text-neutral-300"
>
{label}
</button>
);
})}
</div>
</div>
);
}

View file

@ -0,0 +1,83 @@
/*
All rights reserved to NY0510 (As Known As NY64), 2024
https://github.com/NY0510/ny64.kr/blob/main/src/components/ProfileSection.tsx
*/
import { useState, useEffect } from 'react';
import { useMotionValue, motion, useMotionTemplate } from "framer-motion";
const birthday = new Date('2021-11-14');
export function TimeCounter() {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const background = useMotionTemplate`radial-gradient(200px circle at ${mouseX}px ${mouseY}px, rgba(38, 38, 38, 0.4), transparent 80%)`;
const [afterBirth, setAfterBirth] = useState<string>('');
const [tenThousands, setTenThousands] = useState<number>(0);
const [animate, setAnimate] = useState<boolean>(false);
useEffect(() => {
const interval = setInterval(() => {
const elapsed = new Date().getTime() - birthday.getTime();
setAfterBirth(elapsed.toLocaleString());
const newTenThousands = Math.floor(elapsed / 10000);
if (newTenThousands !== tenThousands) {
setTenThousands(newTenThousands);
setAnimate(true);
setTimeout(() => setAnimate(false), 200);
}
}, 1);
return () => clearInterval(interval);
}, [tenThousands]);
return (
<div
onMouseMove={(e) => {
const { left, top } = e.currentTarget.getBoundingClientRect();
mouseX.set(e.clientX - left);
mouseY.set(e.clientY - top);
}}
className="group relative w-full max-w-[350px] overflow-hidden rounded-xl bg-neutral-950"
title='암냥으로 활동한지'
>
<div className="absolute right-5 top-0 h-px w-80 bg-gradient-to-l from-transparent via-white/30 via-10% to-transparent" />
<motion.div
className="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition duration-300 group-hover:opacity-100"
style={{
background: background,
}}
/>
<div className="relative flexflex-col gap-3 rounded-xl border border-white/10 px-4 py-5">
<div className="space-y-2">
<span> ~ </span>
<p className={`text-sm tabular-nums transition duration-200 ease-in-out ${animate ? 'text-neutral-100' : 'text-neutral-400'}`}>
{afterBirth} ms
</p>
</div>
</div>
</div>
);
/*
return (
<div className="group relative w-fit mx-auto grid overflow-hidden rounded-3xl px-6 py-3 shadow-[0_1000px_0_0_hsl(0_0%_20%)_inset] transition-colors duration-200">
<span>
<span className="spark mask-gradient absolute inset-0 h-[100%] w-[100%] animate-flip overflow-hidden rounded-3xl [mask:linear-gradient(white,_transparent_50%)] before:absolute before:aspect-square before:w-[200%] before:rotate-[-90deg] before:animate-rotate before:bg-[conic-gradient(from_0deg,transparent_0_340deg,white_360deg)] before:content-[''] before:[inset:0_auto_auto_50%] before:[translate:-50%_-15%]" />
</span>
<span className="backdrop absolute inset-px rounded-3xl bg-neutral-950 transition-colors duration-200" />
<span className="z-10 flex flex-col items-center gap-2">
<span> </span>
<span className={`tabular-nums transition duration-200 ease-in-out ${animate ? 'text-neutral-100' : 'text-neutral-400'}`}>
{afterBirth} ms
</span>
</span>
</div>
);*/
}

View file

@ -0,0 +1,52 @@
import { Link } from "react-router";
import { Link as LinkIcon } from "lucide-react";
const events = [
{ date: '2024-12-14', description: '2024 글로벌스타트업학교 K-청소년스타트업 경진대회 우수상 수상', category: "Award", link: "https://www.ncf.or.kr/projects/'2024-%EA%B8%80%EB%A1%9C%EB%B2%8C%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85%ED%95%99%EA%B5%90-k-%EC%B2%AD%EC%86%8C%EB%85%84%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85-%EA%B2%BD%EC%A7%84%EB%8C%80%ED%9A%8C'-%EC%B0%B8%EA%B0%80%EC%9E%90-%EB%AA%A8%EC%A7%91" },
{ date: '2024-12-07', description: '글로벌 스타트업 학교 팀 1위', category: "Award", link: 'https://blog.imnyang.xyz/blog/gss' },
{ date: '2024-12-07', description: '글로벌 스타트업 학교 개인 최우수상', category: "Award", link: 'https://blog.imnyang.xyz/blog/gss' },
{ date: '2024-08-18', description: '29회 해킹캠프 CTF 1위 (고민중독)', category: "Award", link: 'https://ctf.hackingcamp.org/' },
{ date: '2024-08-05', description: '29회 해킹캠프 선발', category: "Conference", link: 'https://hackingcamp.org/' },
{ date: '2024-08-01', description: '글로벌 스타트업 학교 2기 베트남 해외 연수 데모데이 대상 (1위)', category: "Award", link: 'http://ncf.or.kr' },
{ date: '2024-05-16', description: '글로벌 스타트업 학교 2기 합격', category: "Education", link: 'http://ncf.or.kr' },
{ date: '2024-05-11', description: 'LG AI 청소년 캠프 1기 LG 탐색상 수상', category: "Award", link: 'https://lgaiyouthcamp.or.kr/' },
{ date: '2024-05-11', description: 'LG AI 청소년 캠프 1기 수료', category: "Award", link: 'https://lgaiyouthcamp.or.kr/' },
{ date: '2024-04-22', description: '@isangjeong.today (인천상정중학교의 오늘 급식)', category: "Project", link: 'https://www.instagram.com/isangjeong.today/' },
{ date: '2024-03-24', description: 'Dreamhack #133', link: 'https://dreamhack.io/users/40116/wargame' },
{ date: '2024-03-24', description: 'Ubuntu Mirror [Not Working Now]', link: 'https://launchpad.net/ubuntu/+mirror/mirror.imnyang.xyz-release' },
{ date: '2024-03-24', description: '내 목소리로 AI Cover 만들기', category: "Project", link: 'https://colab.research.google.com/drive/1a4G4hD9huBeGRZhEL2HNDMpqSuf4y61k?usp=sharing' },
{ date: '2023-12-20', description: 'LG AI 청소년 캠프 1기 합격', category: "Education" },
{ date: '2023-11-14', description: '인천상정중학교 2023학년도 SW 문제 해결 활동 우수상(2위) 수여', category: "Award" },
{ date: '2023-11-01', description: '블로그 시작', link: 'https://blog.imnyang.xyz', category: "Project" },
{ date: '2023-09-02', description: '선린인터넷고등학교 제6회 소프트웨어나늠축제 Layer7 부서 과정 이수' },
{ date: '2023-07-24', description: '한국정보기술연구원이 주도하는 사이버 가디언즈 보안캠프 수료' },
{ date: '2023-05-15', description: '한국 코드페어 예선 진출' },
{ 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 TimelineComponents() {
return (
<div className="timeline text-white text-left">
{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 to={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>
);
}

76
app/root.tsx Normal file
View file

@ -0,0 +1,76 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";
import stylesheet from "./app.css?url";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
{ rel: "stylesheet", href: stylesheet },
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}

6
app/routes.ts Normal file
View file

@ -0,0 +1,6 @@
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("timeline", "./routes/timeline.tsx"),
] satisfies RouteConfig;

46
app/routes/home.tsx Normal file
View file

@ -0,0 +1,46 @@
import type { Route } from "./+types/home";
import React, { useState } from "react";
import { Link, useNavigate } from "react-router";
import { AnimatedTabs } from "~/components/AnimatedTabs";
import { TimeCounter } from "~/components/TimeCounter";
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export default function Home() {
const [activeTab, setActiveTab] = useState("Home");
const ref = React.useRef(null);
const navigate = useNavigate();
React.useEffect(() => {
if (activeTab === "Timeline") {
navigate("/timeline");
}
}, [activeTab, navigate]);
return (
<div ref={ref} className="flex flex-col w-full items-center">
<header className="w-full md:w-2/5 h-auto flex flex-row justify-between p-5">
<Link to="/" accessKey="h" title="💕 Alt + H" className="text-2xl">💕</Link>
<div className="flex items-center">
<AnimatedTabs activeTab={activeTab} setActiveTab={setActiveTab} />
</div>
</header>
<div id="top" className="w-auto text-center flex items-center justify-center flex-col gap-4">
<img src="https://f.imnya.ng/profile/34b47ba35448cc74a659bcec443c3fbc.webp" alt="imnyang" width={200} height={200} className="rounded-full" />
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-sm text-neutral-400">@imnyang</p>
</div>
<TimeCounter />
<h2 className="text-xl">{`Tab: ${activeTab}`}</h2>
</div>
</div>
);
}

51
app/routes/timeline.tsx Normal file
View file

@ -0,0 +1,51 @@
import type { Route } from "./+types/home";
import React, { useState } from "react";
import { Link, useNavigate } from "react-router";
import { AnimatedTabs } from "~/components/AnimatedTabs";
import { TimeCounter } from "~/components/TimeCounter";
import TimelineComponents from "~/components/TimelineComponents";
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export default function Timeline() {
const [activeTab, setActiveTab] = useState("Timeline");
const ref = React.useRef(null);
const navigate = useNavigate();
React.useEffect(() => {
if (activeTab === "Home") {
navigate("/");
}
}, [activeTab, navigate]);
return (
<div ref={ref} className="flex flex-col w-full items-center">
<header className="w-full md:w-2/5 h-auto flex flex-row justify-between p-5">
<Link to="/" accessKey="h" title="💕 Alt + H" className="text-2xl">💕</Link>
<div className="flex items-center">
<AnimatedTabs activeTab={activeTab} setActiveTab={setActiveTab} />
</div>
</header>
<div id="top" className="w-auto text-center flex items-center justify-center flex-col gap-4">
<img src="https://f.imnya.ng/profile/34b47ba35448cc74a659bcec443c3fbc.webp" alt="imnyang" width={200} height={200} className="rounded-full" />
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-sm text-neutral-400">@imnyang</p>
</div>
<TimeCounter />
<br />
<h1 className="text-2xl font-bold">Timeline</h1>
<br/>
<TimelineComponents />
</div>
</div>
);
}

6
app/utils/cn.ts Normal file
View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

BIN
bun.lockb Executable file

Binary file not shown.

5
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View file

@ -1,19 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "f.imnyang.xyz",
port: "",
pathname: "/profile/**",
},
],
},
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
};
export default nextConfig;

View file

@ -1,33 +1,36 @@
{ {
"name": "imnyang", "name": "imnyang",
"version": "0.1.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev", "build": "cross-env NODE_ENV=production react-router build",
"build": "next build", "dev": "react-router dev",
"start": "next start", "start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js",
"lint": "next lint" "typecheck": "react-router typegen && tsc"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^3.2.3", "@react-router/node": "^7.1.1",
"@emotion/react": "^11.14.0", "@react-router/serve": "^7.1.1",
"@tippyjs/react": "^4.2.6", "clsx": "^2.1.1",
"lucide-react": "^0.468.0", "framer-motion": "^11.15.0",
"next": "15.0.3", "isbot": "^5.1.17",
"next-themes": "^0.4.4", "lucide-react": "^0.469.0",
"react": "19.0.0-rc-66855b96-20241106", "react": "^19.0.0",
"react-dom": "19.0.0-rc-66855b96-20241106", "react-dom": "^19.0.0",
"react-icons": "^5.4.0", "react-router": "^7.1.1",
"tippy.js": "^6.3.7" "tailwind-merge": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.17.9", "@react-router/dev": "^7.1.1",
"@types/react": "^18.3.12", "@types/node": "^20",
"@types/react-dom": "^18.3.1", "@types/react": "^19.0.1",
"eslint": "^8.57.1", "@types/react-dom": "^19.0.1",
"eslint-config-next": "15.0.3", "autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.16",
"typescript": "^5.7.2" "typescript": "^5.7.2",
"vite": "^5.4.11",
"vite-tsconfig-paths": "^5.1.4"
} }
} }

4791
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

7
react-router.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { Config } from "@react-router/dev/config";
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,25 +0,0 @@
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--bg: #101010;
--fg: #fff;
}
.main {
background-color: var(--bg);
color: var(--fg);
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.avatar:hover {
animation: rotate 1000ms linear infinite;
}

View file

@ -1,12 +0,0 @@
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.avatar:hover {
animation: rotate 1000ms linear infinite;
}

View file

@ -1,31 +0,0 @@
import type { Metadata } from "next";
import "./globals.css";
import { Provider } from "@/components/ui/provider"
export const metadata: Metadata = {
title: "imnyang",
description: "imnyang's portfolio",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
</head>
<body
className={`antialiased`}
>
<Provider>{children}</Provider>
</body>
</html>
);
}

View file

@ -1,230 +0,0 @@
'use client';
import React, { useEffect, useState, forwardRef, Ref, Suspense } from "react";
import Image from "next/image";
import Link from "next/link";
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-14', description: '2024 글로벌스타트업학교 K-청소년스타트업 경진대회 우수상 수상', category: "Award", link: "https://www.ncf.or.kr/projects/'2024-%EA%B8%80%EB%A1%9C%EB%B2%8C%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85%ED%95%99%EA%B5%90-k-%EC%B2%AD%EC%86%8C%EB%85%84%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85-%EA%B2%BD%EC%A7%84%EB%8C%80%ED%9A%8C'-%EC%B0%B8%EA%B0%80%EC%9E%90-%EB%AA%A8%EC%A7%91" },
{ date: '2024-12-07', description: '글로벌 스타트업 학교 팀 1위', category: "Award", link: 'https://blog.imnyang.xyz/blog/gss' },
{ date: '2024-12-07', description: '글로벌 스타트업 학교 개인 최우수상', category: "Award", link: 'https://blog.imnyang.xyz/blog/gss' },
{ date: '2024-08-18', description: '29회 해킹캠프 CTF 1위 (고민중독)', category: "Award", link: 'https://ctf.hackingcamp.org/' },
{ date: '2024-08-05', description: '29회 해킹캠프 선발', category: "Conference", link: 'https://hackingcamp.org/' },
{ date: '2024-08-01', description: '글로벌 스타트업 학교 2기 베트남 해외 연수 데모데이 대상 (1위)', category: "Award", link: 'http://ncf.or.kr' },
{ date: '2024-05-16', description: '글로벌 스타트업 학교 2기 합격', category: "Education", link: 'http://ncf.or.kr' },
{ date: '2024-05-11', description: 'LG AI 청소년 캠프 1기 LG 탐색상 수상', category: "Award", link: 'https://lgaiyouthcamp.or.kr/' },
{ date: '2024-05-11', description: 'LG AI 청소년 캠프 1기 수료', category: "Award", link: 'https://lgaiyouthcamp.or.kr/' },
{ date: '2024-04-22', description: '@isangjeong.today (인천상정중학교의 오늘 급식)', category: "Project", link: 'https://www.instagram.com/isangjeong.today/' },
{ date: '2024-03-24', description: 'Dreamhack #133', link: 'https://dreamhack.io/users/40116/wargame' },
{ date: '2024-03-24', description: 'Ubuntu Mirror [Not Working Now]', link: 'https://launchpad.net/ubuntu/+mirror/mirror.imnyang.xyz-release' },
{ date: '2024-03-24', description: '내 목소리로 AI Cover 만들기', category: "Project", link: 'https://colab.research.google.com/drive/1a4G4hD9huBeGRZhEL2HNDMpqSuf4y61k?usp=sharing' },
{ date: '2023-12-20', description: 'LG AI 청소년 캠프 1기 합격', category: "Education" },
{ date: '2023-11-14', description: '인천상정중학교 2023학년도 SW 문제 해결 활동 우수상(2위) 수여', category: "Award" },
{ date: '2023-11-01', description: '블로그 시작', link: 'https://blog.imnyang.xyz', category: "Project" },
{ date: '2023-09-02', description: '선린인터넷고등학교 제6회 소프트웨어나늠축제 Layer7 부서 과정 이수' },
{ date: '2023-07-24', description: '한국정보기술연구원이 주도하는 사이버 가디언즈 보안캠프 수료' },
{ date: '2023-05-15', description: '한국 코드페어 예선 진출' },
{ 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위) 수여' },
];
interface TippyWrapperProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children: React.ReactNode;
ref?: Ref<HTMLAnchorElement>;
}
const TippyWrapper = forwardRef<HTMLAnchorElement, TippyWrapperProps>((props, ref) => (
<a {...props} ref={ref} />
));
TippyWrapper.displayName = 'TippyWrapper';
export default function Home() {
const [userInfo, setUserInfo] = useState({ public_repos: 0, followers: 0 });
const [imageSrc, setImageSrc] = useState("https://f.imnyang.xyz/profile/imnyang.webp");
const [gotoHref, setGotoHref] = useState("/");
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
const updateImageSrc = () => {
if (searchParams.has("kawaii")) {
setImageSrc("https://f.imnyang.xyz/profile/hatchu_imnyang.webp");
setGotoHref("/");
} else {
setImageSrc("https://f.imnyang.xyz/profile/imnyang.webp");
setGotoHref("/?kawaii");
}
if (searchParams.has("no_hair") && searchParams.has("no_ear")) {
setImageSrc("https://f.imnyang.xyz/profile/no_ear_no_long_hair.png");
} else if (searchParams.has("no_ear")) {
setImageSrc("https://f.imnyang.xyz/profile/no_ear.png");
} else if (searchParams.has("no_hair")) {
setImageSrc("https://f.imnyang.xyz/profile/no_hair.avif");
}
if (searchParams.has("fast")) {
const style = document.createElement("style");
style.innerHTML = `
.avatar:hover {
animation: rotate 1ms linear infinite;
}
`;
document.head.appendChild(style);
}
};
updateImageSrc();
}, []);
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();
}, []);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize(); // 초기화 시점에 호출
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
interface SocialLinkProps {
href: string;
icon: string;
tooltip: string;
}
const SocialLink = ({ href, icon, tooltip }: SocialLinkProps) => (
<Tooltip content={tooltip} openDelay={100} positioning={{placement: "bottom"}}>
<Link
href={href}
style={{
color: "#b2a1af",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<i className={icon} style={{ fontSize: "24px" }} />
</Link>
</Tooltip>
);
const [value, setValue] = useState(["about"])
return (
<Suspense fallback={<div>Loading...</div>}>
<div className="main">
<div className="profile flex flex-col items-center gap-4 mt-10 mb-10">
<Image src={imageSrc} width={128} height={128} className="rounded-full avatar" alt="Profile" priority />
<h1 className="text-white text-2xl font-bold">hyun._.suk</h1>
<p>Team. <Link href="https://sqlare.com">Sqlare</Link> & <Link href="https://github.com/objectiveTM">Objective</Link></p>
<div className="flex flex-row gap-6">
{isMobile && (
<SocialLink href="supertoss://send?bank=토스뱅크&accountNo=100079352039&origin=qr" icon="fa-solid fa-circle-dollar-to-slot" tooltip="Toss" />
)}
<SocialLink href={gotoHref} icon="fa-brands fa-github" tooltip={`Github | ${userInfo.public_repos} Repos`} />
<SocialLink href="mailto:me@imnyang.xyz" icon="fa-solid fa-at" tooltip="Mail" />
<SocialLink href="https://instagram.com/fur_local" icon="fa-brands fa-instagram" tooltip="Instagram" />
<SocialLink href="https://x.com/fur_local" icon="fa-brands fa-x-twitter" tooltip="X" />
</div>
</div>
<Stack width="full" maxW="450px" 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:
"안녕하세요. imnyang이라는 닉네임으로 활동중인 남현석입니다.\n",
},
{
"value": "project",
"icon": <ChartBar />,
"title": "Project",
"content":
<div className="flex flex-col text-sky-300">
<p className="text-white">imnyang</p>
<Link href={"https://instagram.com/today.isangjeong"}> </Link>
<Link href={"https://time.imnyang.xyz"}>Local Time</Link>
<Link href={"https://github.com/imnyang/siru"}>Siru</Link>
<Link href={"https://github.com/imnyang/discord-voice-rec"}>Discord Voice Recorder</Link>
<br />
<p className="text-white">Sqlare</p>
<Link href={"https://github.com/sqlare/sqlr.kr/tree/main"}>sqlr.kr (sqlite)</Link>
</div>
},
{
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>,
},
]

View file

@ -1,6 +0,0 @@
import { redirect } from 'next/navigation'
export default async function TimelinePage( ) {
redirect('/')
}

View file

@ -1,47 +0,0 @@
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

View file

@ -1,74 +0,0 @@
"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>
)
},
)

View file

@ -1,40 +0,0 @@
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>
)
},
)

View file

@ -1,25 +0,0 @@
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>
)
},
)

View file

@ -1,17 +0,0 @@
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>
)
})

View file

@ -1,67 +0,0 @@
"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>
)
})

View file

@ -1,62 +0,0 @@
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

View file

@ -1,52 +0,0 @@
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

View file

@ -1,33 +0,0 @@
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>
)
},
)

View file

@ -1,53 +0,0 @@
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>
)
},
)

View file

@ -1,59 +0,0 @@
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

View file

@ -1,15 +0,0 @@
"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>
)
}

View file

@ -1,24 +0,0 @@
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

View file

@ -1,82 +0,0 @@
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>
)
},
)

View file

@ -1,46 +0,0 @@
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>
)
},
)

View file

@ -1,16 +1,36 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
export default { export default {
content: [ content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: { theme: {
extend: { extend: {
colors: { fontFamily: {
background: "var(--background)", sans: [
foreground: "var(--foreground)", '"Inter"',
"ui-sans-serif",
"system-ui",
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
],
},
animation: {
flip: "flip 6s infinite steps(2, end)",
rotate: "rotate 3s linear infinite both",
},
keyframes: {
flip: {
to: {
transform: "rotate(360deg)",
},
},
rotate: {
to: {
transform: "rotate(90deg)",
},
},
}, },
}, },
}, },

View file

@ -1,27 +1,27 @@
{ {
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".react-router/types/**/*"
],
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ES2022"],
"lib": ["dom", "dom.iterable", "esnext"], "types": ["node", "vite/client"],
"allowJs": true, "target": "ES2022",
"skipLibCheck": true, "module": "ES2022",
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "jsx": "react-jsx",
"isolatedModules": true, "rootDirs": [".", "./.react-router/types"],
"jsx": "preserve", "baseUrl": ".",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": { "paths": {
"@/*": ["./src/*"] "~/*": ["./app/*"]
}, },
}, "esModuleInterop": true,
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "verbatimModuleSyntax": true,
"exclude": ["node_modules"] "noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
} }

14
vite.config.ts Normal file
View file

@ -0,0 +1,14 @@
import { reactRouter } from "@react-router/dev/vite";
import autoprefixer from "autoprefixer";
import tailwindcss from "tailwindcss";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
css: {
postcss: {
plugins: [tailwindcss, autoprefixer],
},
},
plugins: [reactRouter(), tsconfigPaths()],
});