Compare commits
77 changed files with 690 additions and 3421 deletions
1
.148
1
.148
|
|
@ -1 +0,0 @@
|
||||||
148
|
|
||||||
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
print-content:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Run uname
|
|
||||||
run: uname -a
|
|
||||||
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install rsync
|
|
||||||
run: |
|
|
||||||
sudo apt-get update && sudo apt-get install -y rsync openssh-client
|
|
||||||
chown -R $(id -u):$(id -g) $GITHUB_WORKSPACE
|
|
||||||
|
|
||||||
- name: Setup SSH Key
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
bun i
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
run: |
|
|
||||||
rsync -avz --delete -e "ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no" "$GITHUB_WORKSPACE/dist/" imnyang@10.11.8.101:/var/static/imnya.ng/
|
|
||||||
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
rm -rf ~/.ssh/id_ed25519
|
|
||||||
26
.gitignore
vendored
26
.gitignore
vendored
|
|
@ -1,24 +1,6 @@
|
||||||
# build output
|
|
||||||
dist/
|
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
# jetbrains setting folder
|
# React Router
|
||||||
.idea/
|
/.react-router/
|
||||||
|
/build/
|
||||||
|
|
|
||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
||||||
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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"]
|
||||||
115
README.md
115
README.md
|
|
@ -1,43 +1,100 @@
|
||||||
# Astro Starter Kit: Minimal
|
# Welcome to React Router!
|
||||||
|
|
||||||
```sh
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
bun create astro@latest -- --template minimal
|
|
||||||
|
[](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
|
||||||
```
|
```
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
### Development
|
||||||
|
|
||||||
## 🚀 Project Structure
|
Start the development server with HMR:
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
```bash
|
||||||
|
npm run dev
|
||||||
```text
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
├── src/
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
## Building for Production
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
Create a production build:
|
||||||
|
|
||||||
## 🧞 Commands
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
## Deployment
|
||||||
|
|
||||||
| Command | Action |
|
### Docker Deployment
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `bun install` | Installs dependencies |
|
|
||||||
| `bun dev` | Starts local dev server at `localhost:4321` |
|
|
||||||
| `bun build` | Build your production site to `./dist/` |
|
|
||||||
| `bun preview` | Preview your build locally, before deploying |
|
|
||||||
| `bun astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
||||||
| `bun astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
This template includes three Dockerfiles optimized for different package managers:
|
||||||
|
|
||||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
- `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));
|
||||||
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
import { defineConfig } from "astro/config";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
vite: {
|
|
||||||
plugins: [tailwindcss()],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue