diff --git a/.gitignore b/.gitignore
index 5ef6a52..1ec5eb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+certificates
\ No newline at end of file
diff --git a/bun.lock b/bun.lock
index 9a2e975..59b338e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -40,6 +40,7 @@
"lucide-react": "^0.544.0",
"next": "15.5.4",
"next-themes": "^0.4.6",
+ "radix-ui": "^1.4.3",
"react": "19.1.0",
"react-day-picker": "^9.11.0",
"react-dom": "19.1.0",
@@ -180,6 +181,8 @@
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
+ "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
+
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
@@ -214,6 +217,8 @@
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
+ "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
+
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
@@ -226,6 +231,10 @@
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
+ "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
+
+ "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
+
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
@@ -256,10 +265,14 @@
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
+ "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
+
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
+ "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
+
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
@@ -464,6 +477,8 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+ "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
+
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-day-picker": ["react-day-picker@9.11.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-L4FYOaPrr3+AEROeP6IG2mCORZZfxJDkJI2df8mv1jyPrNYeccgmFPZDaHyAuPCBCddQFozkxbikj2NhMEYfDQ=="],
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..10025d9 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
+ allowedDevOrigins: ['imnyang.dev'],
};
export default nextConfig;
diff --git a/package.json b/package.json
index f6c2929..9dee9a8 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"lucide-react": "^0.544.0",
"next": "15.5.4",
"next-themes": "^0.4.6",
+ "radix-ui": "^1.4.3",
"react": "19.1.0",
"react-day-picker": "^9.11.0",
"react-dom": "19.1.0",
diff --git a/public/background/1.avif b/public/background/1.avif
new file mode 100644
index 0000000..c40b70d
Binary files /dev/null and b/public/background/1.avif differ
diff --git a/public/background/10.avif b/public/background/10.avif
new file mode 100644
index 0000000..07bba24
Binary files /dev/null and b/public/background/10.avif differ
diff --git a/public/background/11.avif b/public/background/11.avif
new file mode 100644
index 0000000..c04cb49
Binary files /dev/null and b/public/background/11.avif differ
diff --git a/public/background/12.avif b/public/background/12.avif
new file mode 100644
index 0000000..3c0008e
Binary files /dev/null and b/public/background/12.avif differ
diff --git a/public/background/13.avif b/public/background/13.avif
new file mode 100644
index 0000000..5349006
Binary files /dev/null and b/public/background/13.avif differ
diff --git a/public/background/14.avif b/public/background/14.avif
new file mode 100644
index 0000000..e533667
Binary files /dev/null and b/public/background/14.avif differ
diff --git a/public/background/2.avif b/public/background/2.avif
new file mode 100644
index 0000000..c2e74c7
Binary files /dev/null and b/public/background/2.avif differ
diff --git a/public/background/3.avif b/public/background/3.avif
new file mode 100644
index 0000000..9b9b8bb
Binary files /dev/null and b/public/background/3.avif differ
diff --git a/public/background/4.avif b/public/background/4.avif
new file mode 100644
index 0000000..846c9b5
Binary files /dev/null and b/public/background/4.avif differ
diff --git a/public/background/5.avif b/public/background/5.avif
new file mode 100644
index 0000000..261193e
Binary files /dev/null and b/public/background/5.avif differ
diff --git a/public/background/6.avif b/public/background/6.avif
new file mode 100644
index 0000000..fbbb8cf
Binary files /dev/null and b/public/background/6.avif differ
diff --git a/public/background/7.avif b/public/background/7.avif
new file mode 100644
index 0000000..b386682
Binary files /dev/null and b/public/background/7.avif differ
diff --git a/public/background/8.avif b/public/background/8.avif
new file mode 100644
index 0000000..2e9792f
Binary files /dev/null and b/public/background/8.avif differ
diff --git a/public/background/9.avif b/public/background/9.avif
new file mode 100644
index 0000000..a5f4cf2
Binary files /dev/null and b/public/background/9.avif differ
diff --git a/public/bg.avif b/public/bg.avif
new file mode 100644
index 0000000..e2c685d
Binary files /dev/null and b/public/bg.avif differ
diff --git a/public/bg.png b/public/bg.png
new file mode 100644
index 0000000..35bbba2
Binary files /dev/null and b/public/bg.png differ
diff --git a/public/char.avif b/public/char.avif
new file mode 100644
index 0000000..7e9208d
Binary files /dev/null and b/public/char.avif differ
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
index 718d6fe..0e5a95c 100644
Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ
diff --git a/src/app/globals.css b/src/app/globals.css
index cfb4661..0c750d9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,12 +1,12 @@
+@import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css");
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@font-face {
- font-family: 'NType82Headline';
- src: url('https://f.imnya.ng/font/NType82-Headline.woff2') format('woff2');
-
+ font-family: "NType82Headline";
+ src: url("https://f.imnya.ng/font/NType82-Headline.woff2") format("woff2");
}
.font-ntype {
@@ -14,43 +14,6 @@
}
:root {
- --background: hsl(340 40% 98%);
- --foreground: hsl(315 21% 8%);
- --card: hsl(340 40% 98%);
- --card-foreground: hsl(315 21% 8%);
- --popover: hsl(340 40% 98%);
- --popover-foreground: hsl(315 21% 8%);
- --primary: hsl(340 25% 15%);
- --primary-foreground: hsl(0 0% 98%);
- --secondary: hsl(340 25% 95%);
- --secondary-foreground: hsl(240 5.9% 10%);
- --muted: hsl(340 20% 95%);
- --muted-foreground: hsl(340 10% 60%);
- --accent: hsl(340 25% 94%);
- --accent-foreground: hsl(240 5.9% 10%);
- --destructive: hsl(0 84.2% 60.2%);
- --destructive-foreground: hsl(0 0% 98%);
- --border: hsl(340 25% 90%);
- --input: hsl(340 25% 90%);
- --ring: hsl(315 21% 8%);
- --chart-1: hsl(12 76% 61%);
- --chart-2: hsl(173 58% 39%);
- --chart-3: hsl(197 37% 24%);
- --chart-4: hsl(43 74% 66%);
- --chart-5: hsl(27 87% 67%);
- --radius: 0.6rem;
- --sidebar-background: hsl(340 25% 98%);
- --sidebar-foreground: hsl(240 5.3% 26.1%);
- --sidebar-primary: hsl(240 5.9% 10%);
- --sidebar-primary-foreground: hsl(0 0% 98%);
- --sidebar-accent: hsl(340 20% 95%);
- --sidebar-accent-foreground: hsl(240 5.9% 10%);
- --sidebar-border: hsl(340 20% 90%);
- --sidebar-ring: hsl(217.2 91.2% 59.8%);
- --sidebar: hsl(0 0% 98%);
-}
-
-.dark {
--background: hsl(315 21% 8%);
--foreground: hsl(0 0% 98%);
--card: hsl(315 21% 8%);
@@ -151,5 +114,9 @@
}
body {
@apply bg-background text-foreground;
+ font-family: "Wanted Sans Variable", "Wanted Sans", -apple-system,
+ BlinkMacSystemFont, system-ui, "Segoe UI", "Apple SD Gothic Neo",
+ "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji",
+ "Segoe UI Symbol", sans-serif;
}
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 65d2a11..ce83b95 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import "./globals.css";
-import Sidebar from "@/components/sidebar";
export const metadata: Metadata = {
title: "남현석 | :two_hearts: imnya.ng",
@@ -15,7 +14,6 @@ export default function RootLayout({
return (
-
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index fb1335a..ca85446 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,18 +1,26 @@
+import NeoFetch from "@/components/NeoFetch";
+import TimelineComponent from "@/components/timeline";
+import Top from "@/components/Top";
+
export default function Home() {
return (
-
-
-
-
About
-
항상 새로운 것을 찾고 삶을 더 간단명료하게 만들고 있는 학생 개발자 남현석입니다.
-
+
+
+
);
}
diff --git a/src/app/timeline/route.ts b/src/app/timeline/route.ts
new file mode 100644
index 0000000..0daf1c8
--- /dev/null
+++ b/src/app/timeline/route.ts
@@ -0,0 +1,3 @@
+export const timeline = () => {
+ window.location.hash = '#timeline';
+};
\ No newline at end of file
diff --git a/src/components/NeoFetch.tsx b/src/components/NeoFetch.tsx
new file mode 100644
index 0000000..402c9a8
--- /dev/null
+++ b/src/components/NeoFetch.tsx
@@ -0,0 +1,110 @@
+"use client";
+import { events } from "@/lib/events";
+import { useIpData } from "../hooks/use-ip-data";
+import { useWakaTimeData } from "../hooks/use-wakatime-data";
+
+export default function NeoFetch() {
+ const ipData = useIpData();
+ const wakaTimeData = useWakaTimeData();
+
+ return (
+
+
+
+
+
+ imnyang@adofai.gg
+
+
----------
+
+
+ Uptime:{" "}
+ {(() => {
+ const startDate = new Date("2010-11-08T00:00:00+09:00");
+ const now = new Date();
+ let diff = now.getTime() - startDate.getTime();
+
+ const years = Math.floor(diff / (1000 * 60 * 60 * 24 * 365));
+ diff %= 1000 * 60 * 60 * 24 * 365;
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+ diff %= 1000 * 60 * 60 * 24;
+ const hours = Math.floor(diff / (1000 * 60 * 60));
+ diff %= 1000 * 60 * 60;
+ const mins = Math.floor(diff / (1000 * 60));
+
+ return `${years} years, ${days} days, ${hours} hours, ${mins} mins`;
+ })()}
+
+
+
+ Experience:{" "}
+ {Object.values(events).flat().length}
+
+
+
+ WakaTime:{" "}
+ {wakaTimeData?.data?.total_seconds_including_other_language
+ ? (() => {
+ const seconds = wakaTimeData.data.total_seconds_including_other_language;
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ return `${days} days, ${hours} hours, ${minutes} minutes, ${Math.round(
+ seconds % 60
+ )} seconds`;
+ })()
+ : "N/A"}
+
+
+
+ Most used Language:{" "}
+ {wakaTimeData?.data?.languages[0]?.name || "N/A"}{" "}
+ {wakaTimeData?.data?.languages[0]?.total_seconds
+ ? (() => {
+ const seconds = wakaTimeData.data.languages[0].total_seconds;
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ return `(${days} days, ${hours} hours, ${minutes} minutes, ${Math.round(
+ seconds % 60
+ )} seconds)`;
+ })()
+ : "N/A"}
+
+
+
+ Terminal: {ipData}
+
+
+
+ Locale: ko_KR.UTF-8
+
+
+
+
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
⠀⠀
+
+
+
+
+ );
+}
diff --git a/src/components/Top.tsx b/src/components/Top.tsx
new file mode 100644
index 0000000..02f1032
--- /dev/null
+++ b/src/components/Top.tsx
@@ -0,0 +1,105 @@
+"use client";
+import Sidebar from "@/components/sidebar";
+import Image from "next/image";
+import { useEffect, useRef, useState } from "react";
+
+export default function Top() {
+ const containerRef = useRef(null);
+ const [randomBg, setRandomBg] = useState(1);
+
+ useEffect(() => {
+ setRandomBg(Math.floor(Math.random() * 14) + 1);
+ }, []);
+
+ const requestPermission = async () => {
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+
+ if (!isIOS)
+ window.addEventListener("deviceorientation", handleDeviceOrientation);
+
+ try {
+ const res = await (DeviceOrientationEvent as any).requestPermission();
+ if (res === "granted") {
+ window.addEventListener("devicemotion", handleDeviceMotion);
+ window.addEventListener("deviceorientation", handleDeviceOrientation);
+ }
+ } catch {
+ window.addEventListener("deviceorientation", handleDeviceOrientation);
+ }
+ };
+
+ const handleDeviceOrientation = (event: DeviceOrientationEvent) => {
+ if (!containerRef.current) return;
+ const gamma = event.gamma || 0;
+ const beta = event.beta || 0;
+
+ const x = (gamma / 90) * -50;
+ const y = (beta / 180) * -50;
+
+ const bg = containerRef.current;
+ bg.style.backgroundPosition = `${50 + x}% ${50 + y}%`;
+
+ const img = bg.querySelector("img");
+ if (img) img.style.transform = `translate(${x * 2}px, ${y * 2}px)`;
+ };
+
+ const handleDeviceMotion = (event: DeviceMotionEvent) => {
+ if (!containerRef.current || !event.accelerationIncludingGravity) return;
+ const { x, y } = event.accelerationIncludingGravity;
+ if (x === null || y === null) return;
+ const bg = containerRef.current;
+ bg.style.backgroundPosition = `${50 - x * 3}% ${50 - y * 3}%`;
+ };
+
+ useEffect(() => {
+ return () => {
+ window.removeEventListener("deviceorientation", handleDeviceOrientation);
+ window.removeEventListener("devicemotion", handleDeviceMotion);
+ };
+ }, []);
+
+ return (
+ {
+ e.preventDefault();
+ window.scrollBy(0, window.innerHeight * (e.deltaY > 0 ? 1 : -1));
+ }}
+ >
+
+ {
+ const { clientX, clientY } = e;
+ const { innerWidth, innerHeight } = window;
+ const x = (clientX / innerWidth - 0.5) * -50;
+ const y = (clientY / innerHeight - 0.5) * -50;
+
+ const bg = e.currentTarget;
+ if (bg) {
+ bg.style.backgroundPosition = `${50 + x}% ${50 + y}%`;
+ }
+ const img = e.currentTarget.querySelector("img");
+ if (img) {
+ img.style.transform = `translate(${x}px, ${y}px)`;
+ }
+ }}
+ >
+
+
+
+ );
+}
diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx
index 75393b8..fb0a9a0 100644
--- a/src/components/sidebar.tsx
+++ b/src/components/sidebar.tsx
@@ -1,12 +1,26 @@
import Image from "next/image";
+import { Popover } from "./ui/popover";
export default function Sidebar() {
- return (
-
-
-
+ return (
+
+
+
+
- )
-}
\ No newline at end of file
+
+
+
+ );
+}
diff --git a/src/components/timeline.tsx b/src/components/timeline.tsx
new file mode 100644
index 0000000..4350af3
--- /dev/null
+++ b/src/components/timeline.tsx
@@ -0,0 +1,86 @@
+"use client";
+import { events } from "@/lib/events";
+import { LinkIcon } from "lucide-react";
+import { useEffect, useState, useRef } from "react";
+
+export default function TimelineComponent() {
+ const [selectedYear, setSelectedYear] = useState
(null);
+
+ const years = Array.from(
+ new Set(events.map((event) => new Date(event.date).getFullYear()))
+ ).sort((a, b) => b - a);
+
+ useEffect(() => {
+ if (years.length > 0 && selectedYear === null) {
+ setSelectedYear(years[0]);
+ }
+ }, [years, selectedYear]);
+
+ const filteredEvents = selectedYear
+ ? events.filter(
+ (event) => new Date(event.date).getFullYear() === selectedYear
+ )
+ : [];
+
+ return (
+
+
+
🌠 수상 및 교육
+
+
+ {/* Left column - Year buttons */}
+
+ {years.map((year) => (
+
+ ))}
+
+
+ {/* Right column - Events */}
+
+
+ {filteredEvents.map((event, index) => (
+
+
+
+ {new Date(event.date).toLocaleDateString("en-US", {
+ month: "short",
+ day: "2-digit",
+ })}
+
+
+ ㆍ{event.category}
+
+
+ {event.link ? (
+
+ {event.description}{" "}
+
+
+ ) : (
+
{event.description}
+ )}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/timeline.tsx b/src/components/ui/timeline.tsx
new file mode 100644
index 0000000..89bdfe2
--- /dev/null
+++ b/src/components/ui/timeline.tsx
@@ -0,0 +1,210 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+// Types
+type TimelineContextValue = {
+ activeStep: number
+ setActiveStep: (step: number) => void
+}
+
+// Context
+const TimelineContext = React.createContext(
+ undefined
+)
+
+const useTimeline = () => {
+ const context = React.useContext(TimelineContext)
+ if (!context) {
+ throw new Error("useTimeline must be used within a Timeline")
+ }
+ return context
+}
+
+// Components
+interface TimelineProps extends React.HTMLAttributes {
+ defaultValue?: number
+ value?: number
+ onValueChange?: (value: number) => void
+ orientation?: "horizontal" | "vertical"
+}
+
+function Timeline({
+ defaultValue = 1,
+ value,
+ onValueChange,
+ orientation = "vertical",
+ className,
+ ...props
+}: TimelineProps) {
+ const [activeStep, setInternalStep] = React.useState(defaultValue)
+
+ const setActiveStep = React.useCallback(
+ (step: number) => {
+ if (value === undefined) {
+ setInternalStep(step)
+ }
+ onValueChange?.(step)
+ },
+ [value, onValueChange]
+ )
+
+ const currentStep = value ?? activeStep
+
+ return (
+
+
+
+ )
+}
+
+// TimelineContent
+function TimelineContent({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+// TimelineDate
+interface TimelineDateProps extends React.HTMLAttributes {
+ asChild?: boolean
+}
+
+function TimelineDate({
+ asChild = false,
+ className,
+ ...props
+}: TimelineDateProps) {
+ const Comp = asChild ? Slot.Root : "time"
+
+ return (
+
+ )
+}
+
+// TimelineHeader
+function TimelineHeader({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+// TimelineIndicator
+interface TimelineIndicatorProps extends React.HTMLAttributes {
+ asChild?: boolean
+}
+
+function TimelineIndicator({
+ asChild = false,
+ className,
+ children,
+ ...props
+}: TimelineIndicatorProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+// TimelineItem
+interface TimelineItemProps extends React.HTMLAttributes {
+ step: number
+}
+
+function TimelineItem({ step, className, ...props }: TimelineItemProps) {
+ const { activeStep } = useTimeline()
+
+ return (
+
+ )
+}
+
+// TimelineSeparator
+function TimelineSeparator({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+// TimelineTitle
+function TimelineTitle({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export {
+ Timeline,
+ TimelineContent,
+ TimelineDate,
+ TimelineHeader,
+ TimelineIndicator,
+ TimelineItem,
+ TimelineSeparator,
+ TimelineTitle,
+}
\ No newline at end of file
diff --git a/src/hooks/use-ip-data.ts b/src/hooks/use-ip-data.ts
new file mode 100644
index 0000000..1880e16
--- /dev/null
+++ b/src/hooks/use-ip-data.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from "react";
+
+export function useIpData() {
+ const [ipData, setIpData] = useState("");
+
+ useEffect(() => {
+ const fetchIpData = async () => {
+ try {
+ const response = await fetch("https://api.imnya.ng/ip");
+ const data = await response.text();
+ setIpData(data);
+ } catch (error) {
+ console.error("Failed to fetch IP data:", error);
+ }
+ };
+
+ fetchIpData();
+ }, []);
+
+ return ipData;
+}
\ No newline at end of file
diff --git a/src/hooks/use-wakatime-data.ts b/src/hooks/use-wakatime-data.ts
new file mode 100644
index 0000000..c4a3fc5
--- /dev/null
+++ b/src/hooks/use-wakatime-data.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from "react";
+
+export function useWakaTimeData() {
+ const [wakaTimeData, setWakaTimeData] = useState(null);
+
+ useEffect(() => {
+ const fetchWakaTimeData = async () => {
+ try {
+ const response = await fetch("https://api.imnya.ng/wakatime");
+ const data = await response.json();
+ setWakaTimeData(data);
+ } catch (error) {
+ console.error("Failed to fetch WakaTime data:", error);
+ }
+ };
+
+ fetchWakaTimeData();
+ }, []);
+
+ return wakaTimeData;
+}
\ No newline at end of file
diff --git a/src/lib/events.ts b/src/lib/events.ts
new file mode 100644
index 0000000..e2308c4
--- /dev/null
+++ b/src/lib/events.ts
@@ -0,0 +1,146 @@
+export const events = [
+ {
+ description: "화이트햇스쿨 3기 이수",
+ category: "Education",
+ date: "2025-09-24",
+ link: "https://whitehatschool.kr/home/kor/main.do",
+ },
+ {
+ date: "2025-09-13",
+ description: "선린인터넷고 소프트웨어 나눔축제 AnA 이수",
+ category: "Education",
+ link: "https://ssf.sunrin.io/",
+ },
+ {
+ date: "2025-07-26",
+ description: "선린인터넷고 여름방학 중학생 특별교육 우수 이수 (프로그래밍)",
+ category: "Education",
+ link: "https://sunrint.sen.hs.kr/",
+ },
+ {
+ date: "2025-01-19",
+ description: "2024 Sunrin LOGCON(TeamLog 주최) 중등부 3위",
+ category: "Award",
+ link: "https://teamlog.kr",
+ },
+ {
+ date: "2025-01-12",
+ description: "2024 Sunrin Layer7 CTF 중등부 2위",
+ category: "Award",
+ link: "https://layer7.kr",
+ },
+ {
+ date: "2025-01-10",
+ description: "선린인터넷고 겨울방학 중학생 특별교육 이수 (IT경영학과)",
+ category: "Education",
+ link: "https://sunrint.sen.hs.kr/",
+ },
+ {
+ 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://ncf.or.kr",
+ },
+ {
+ date: "2024-12-07",
+ description: "글로벌 스타트업 학교 개인 최우수상",
+ category: "Award",
+ link: "https://ncf.or.kr",
+ },
+ {
+ date: "2024-08-18",
+ description: "29회 해킹캠프 CTF 1위 (고민중독)",
+ category: "Award & Conference",
+ link: "https://ctf.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 & Education",
+ link: "https://lgaiyouthcamp.or.kr/",
+ },
+ {
+ date: "2023-11-14",
+ description: "인천상정중학교 2023학년도 SW 문제 해결 활동 우수상(2위) 수여",
+ category: "Award",
+ },
+ {
+ date: "2023-09-02",
+ description:
+ "선린인터넷고등학교 제6회 소프트웨어나눔축제 Layer7 부서 과정 이수",
+ category: "Education",
+ },
+ {
+ date: "2023-07-24",
+ description: "한국정보기술연구원이 주도하는 사이버 가디언즈 보안캠프 수료",
+ category: "Education",
+ },
+ {
+ date: "2023-05-15",
+ description: "한국 코드페어 예선 진출",
+ category: "Award",
+ },
+ {
+ date: "2022-12-20",
+ description: "2022 SW영재 창작대회 은상 수상",
+ category: "Award",
+ },
+ {
+ date: "2022-09-27",
+ description: "2022 삼성 주니어 SW 창작대회 본선 진출",
+ category: "Award",
+ },
+ {
+ date: "2022-05-23",
+ description: "2022학년도 석정초SW영재학급 첫 수업",
+ category: "Education",
+ },
+ {
+ date: "2022-07-26",
+ description: "제 14회 맑은하늘 맑은웃음 공모전에서 맑은웃음상 수여",
+ category: "Award",
+ },
+ {
+ date: "2021-11-14",
+ description: "Become a ZEPETO Creator 이수",
+ category: "Education",
+ },
+ {
+ date: "2021-05-19",
+ description:
+ "소프트웨어와 전자신문이 주관한 소프트웨어재단 꿈찾기 캠프 이수",
+ category: "Education",
+ },
+ {
+ date: "2018-01-27",
+ description:
+ "제4회 맑은하늘 맑은웃음 어린이 문예공모전에서 위닉스상(2위) 수여",
+ category: "Award",
+ },
+];
\ No newline at end of file