commit
4d491badd2
48 changed files with 683 additions and 5917 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
|
|
@ -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
42
.gitignore
vendored
|
|
@ -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
22
Dockerfile
Normal 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
25
Dockerfile.bun
Normal 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
26
Dockerfile.pnpm
Normal 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
101
README.md
|
|
@ -1 +1,100 @@
|
||||||
# A
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](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
9
app/app.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
color-scheme: dark;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
71
app/components/AnimatedTabs.tsx
Normal file
71
app/components/AnimatedTabs.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
app/components/TimeCounter.tsx
Normal file
83
app/components/TimeCounter.tsx
Normal 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>
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
|
||||||
52
app/components/TimelineComponents.tsx
Normal file
52
app/components/TimelineComponents.tsx
Normal 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
76
app/root.tsx
Normal 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
6
app/routes.ts
Normal 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
46
app/routes/home.tsx
Normal 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
51
app/routes/timeline.tsx
Normal 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
6
app/utils/cn.ts
Normal 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
BIN
bun.lockb
Executable file
Binary file not shown.
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal 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.
|
||||||
|
|
@ -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;
|
|
||||||
49
package.json
49
package.json
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue