mirror of
https://github.com/dyzulk/dyzulk.github.io.git
synced 2026-01-26 05:45:28 +07:00
feat: Implement public site structure with Home, Blog, and Projects pages, global search, and markdown rendering.
This commit is contained in:
627
package-lock.json
generated
627
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"framer-motion": "^12.26.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^19.2.0",
|
||||
@@ -25,6 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -1085,6 +1087,447 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||
"license": "MIT",
|
||||
"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-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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
|
||||
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.4"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.53",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||
@@ -1522,6 +1965,33 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1650,7 +2120,7 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -2067,6 +2537,18 @@
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.23",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||
@@ -2364,6 +2846,22 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-id": "^1.1.0",
|
||||
"@radix-ui/react-primitive": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2521,6 +3019,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
@@ -3030,6 +3534,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -5163,6 +5676,53 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||
@@ -5201,6 +5761,28 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -5908,6 +6490,49 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"framer-motion": "^12.26.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^19.2.0",
|
||||
@@ -27,6 +28,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
@@ -22,6 +22,14 @@ export default function Navbar() {
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-1 pr-1">
|
||||
<button
|
||||
onClick={() => document.dispatchEvent(new Event('open-search-palette'))}
|
||||
className="p-2 mr-1 text-zinc-400 hover:text-white hover:bg-white/5 rounded-full transition-colors"
|
||||
aria-label="Search"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</button>
|
||||
|
||||
{links.map((link) => {
|
||||
const isActive = location.pathname === link.href
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import Navbar from './Navbar'
|
||||
import { SearchCommand } from '@/components/ui/search-command'
|
||||
|
||||
export default function PublicLayout() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-background text-foreground selection:bg-white/20">
|
||||
<SearchCommand />
|
||||
<Navbar />
|
||||
<main className="flex-1 pt-24 pb-12">
|
||||
<Outlet />
|
||||
|
||||
@@ -9,23 +9,36 @@ interface MarkdownViewerProps {
|
||||
|
||||
export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
|
||||
return (
|
||||
<div className={cn("prose prose-slate dark:prose-invert max-w-none", className)}>
|
||||
<div className={cn("prose prose-invert max-w-none prose-headings:font-bold prose-headings:tracking-tight prose-a:text-blue-400 prose-img:rounded-xl prose-img:border prose-img:border-white/10", className)}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
img: ({node, ...props}) => (
|
||||
<img
|
||||
{...props}
|
||||
className="rounded-lg shadow-md my-6 w-full object-cover max-h-[500px]"
|
||||
className="rounded-xl shadow-lg my-8 w-full object-cover max-h-[600px] bg-white/5 border border-white/10"
|
||||
loading="lazy"
|
||||
/>
|
||||
),
|
||||
a: ({node, ...props}) => (
|
||||
<a {...props} className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer" />
|
||||
<a {...props} className="text-blue-400 hover:text-blue-300 underline decoration-blue-400/30 hover:decoration-blue-400 transition-all" target="_blank" rel="noopener noreferrer" />
|
||||
),
|
||||
pre: ({node, ...props}) => (
|
||||
<pre {...props} className="bg-slate-900 text-slate-50 p-4 rounded-lg overflow-x-auto my-4" />
|
||||
)
|
||||
<pre {...props} className="bg-zinc-950/50 border border-white/10 p-4 rounded-xl overflow-x-auto my-6" />
|
||||
),
|
||||
code: ({node, ...props}) => {
|
||||
const {className, children, ...rest} = props
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return match ? (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<code {...rest} className="px-1.5 py-0.5 rounded-md bg-white/10 text-sm font-mono text-white/90">
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
|
||||
131
src/components/ui/search-command.tsx
Normal file
131
src/components/ui/search-command.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Command } from 'cmdk'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { Search, FileText, Code, Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function SearchCommand() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<{ projects: any[], posts: any[] }>({ projects: [], posts: [] })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
}
|
||||
|
||||
const openEvent = () => setOpen(true)
|
||||
|
||||
document.addEventListener('keydown', down)
|
||||
document.addEventListener('open-search-palette', openEvent)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', down)
|
||||
document.removeEventListener('open-search-palette', openEvent)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (query.length === 0) {
|
||||
setResults({ projects: [], posts: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const search = async () => {
|
||||
setLoading(true)
|
||||
const [projectsRes, postsRes] = await Promise.all([
|
||||
supabase.from('projects').select('id, title').ilike('title', `%${query}%`).limit(5),
|
||||
supabase.from('posts').select('id, title, slug').ilike('title', `%${query}%`).limit(5)
|
||||
])
|
||||
|
||||
setResults({
|
||||
projects: projectsRes.data || [],
|
||||
posts: postsRes.data || []
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const timeout = setTimeout(search, 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query])
|
||||
|
||||
const runCommand = (command: () => void) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
}
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] px-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setOpen(false)} />
|
||||
|
||||
<div className="relative w-full max-w-lg overflow-hidden rounded-xl bg-[#0a0a0a] border border-white/10 shadow-2xl animate-in fade-in zoom-in-95 duration-200">
|
||||
<Command label="Global Search" className="w-full">
|
||||
<div className="flex items-center border-b border-white/10 px-4">
|
||||
<Search className="mr-2 h-5 w-5 shrink-0 text-white/50" />
|
||||
<Command.Input
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
placeholder="Search projects or posts..."
|
||||
className="flex h-14 w-full rounded-md bg-transparent py-3 text-sm outline-none text-white placeholder:text-neutral-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Command.List className="max-h-[300px] overflow-y-auto p-2 scrollbar-hide">
|
||||
{loading && <div className="py-6 text-center text-sm text-neutral-500 flex items-center justify-center gap-2"><Loader2 className="animate-spin" size={14}/> Searching...</div>}
|
||||
{!loading && query && results.projects.length === 0 && results.posts.length === 0 && (
|
||||
<div className="py-6 text-center text-sm text-neutral-500">No results found.</div>
|
||||
)}
|
||||
|
||||
{results.projects.length > 0 && (
|
||||
<Command.Group heading="Projects" className="text-xs font-medium text-neutral-500 px-2 py-1.5 mb-1">
|
||||
{results.projects.map((project) => (
|
||||
<Command.Item
|
||||
key={project.id}
|
||||
onSelect={() => runCommand(() => navigate('/projects'))}
|
||||
className="flex items-center gap-3 rounded-lg px-2 py-2 text-sm text-neutral-200 aria-selected:bg-white/10 aria-selected:text-white cursor-pointer transition-colors"
|
||||
>
|
||||
<Code size={14} className="opacity-50" />
|
||||
{project.title}
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{results.posts.length > 0 && (
|
||||
<Command.Group heading="Blog Posts" className="text-xs font-medium text-neutral-500 px-2 py-1.5 mb-1">
|
||||
{results.posts.map((post) => (
|
||||
<Command.Item
|
||||
key={post.id}
|
||||
onSelect={() => runCommand(() => navigate(`/blog/${post.slug}`))}
|
||||
className="flex items-center gap-3 rounded-lg px-2 py-2 text-sm text-neutral-200 aria-selected:bg-white/10 aria-selected:text-white cursor-pointer transition-colors"
|
||||
>
|
||||
<FileText size={14} className="opacity-50" />
|
||||
{post.title}
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{!query && (
|
||||
<div className="py-10 text-center text-sm text-neutral-600">
|
||||
<p>Type to search...</p>
|
||||
<div className="mt-2 text-xs flex justify-center gap-2">
|
||||
<span className="bg-white/5 px-2 py-1 rounded border border-white/5">Projects</span>
|
||||
<span className="bg-white/5 px-2 py-1 rounded border border-white/5">Blog</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Loader2, Calendar } from 'lucide-react'
|
||||
import { Loader2, Calendar, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function Blog() {
|
||||
const [posts, setPosts] = useState<any[]>([])
|
||||
@@ -24,42 +24,55 @@ export default function Blog() {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (loading) return <div className="flex justify-center py-20"><Loader2 className="animate-spin" /></div>
|
||||
if (loading) return <div className="flex justify-center items-center h-[50vh]"><Loader2 className="animate-spin text-white" /></div>
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12 max-w-4xl">
|
||||
<h1 className="text-4xl font-bold mb-8 text-center">Blog & Tutorials</h1>
|
||||
<div className="container mx-auto px-4 py-20 max-w-4xl">
|
||||
<div className="mb-16 text-center">
|
||||
<h1 className="text-5xl font-bold mb-4 tracking-tight bg-clip-text text-transparent bg-gradient-to-b from-white to-white/50">Writing</h1>
|
||||
<p className="text-muted-foreground">Thoughts, tutorials, and insights on development.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
{posts.map((post) => (
|
||||
<article key={post.id} className="flex flex-col md:flex-row gap-6 border-b border-slate-100 dark:border-slate-800 pb-8 last:border-0 hover:bg-slate-50 dark:hover:bg-slate-900/50 p-4 rounded-xl transition-colors">
|
||||
{post.cover_image && (
|
||||
<div className="w-full md:w-48 h-32 flex-shrink-0 rounded-lg overflow-hidden bg-slate-200">
|
||||
<img src={post.cover_image} alt={post.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 mb-2">
|
||||
<Calendar size={14} />
|
||||
{new Date(post.created_at).toLocaleDateString()}
|
||||
<div className="flex gap-2">
|
||||
{post.tags?.map((tag: string) => (
|
||||
<span key={tag} className="text-blue-500 font-medium">#{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Link to={`/blog/${post.slug}`}>
|
||||
<h2 className="text-2xl font-bold mb-2 hover:text-blue-600 transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
</Link>
|
||||
<p className="text-slate-600 dark:text-slate-400 line-clamp-2">
|
||||
{post.excerpt || 'No description available.'}
|
||||
</p>
|
||||
<Link to={`/blog/${post.slug}`} className="inline-block mt-4 text-sm font-medium text-blue-600 hover:underline">
|
||||
Read Article →
|
||||
</Link>
|
||||
</div>
|
||||
<article key={post.id} className="group relative">
|
||||
<Link to={`/blog/${post.slug}`} className="block p-8 rounded-2xl bg-white/5 border border-white/10 hover:bg-white/10 hover:border-white/20 transition-all duration-300">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
{/* Content */}
|
||||
<div className="flex-1 order-2 md:order-1">
|
||||
<div className="flex items-center gap-3 text-xs font-mono text-muted-foreground mb-4">
|
||||
<span>{new Date(post.created_at).toLocaleDateString()}</span>
|
||||
{post.tags?.length > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-primary/80">#{post.tags[0]}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3 text-white group-hover:text-blue-400 transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed line-clamp-2 md:line-clamp-3 mb-6">
|
||||
{post.excerpt || 'No description available.'}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-white/80 group-hover:text-white group-hover:translate-x-1 transition-all">
|
||||
Read Article <ArrowRight size={16} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional Image */}
|
||||
{post.cover_image && (
|
||||
<div className="w-full md:w-48 h-32 md:h-auto flex-shrink-0 order-1 md:order-2">
|
||||
<div className="w-full h-full rounded-xl overflow-hidden bg-white/5 border border-white/5">
|
||||
<img src={post.cover_image} alt={post.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { MarkdownViewer } from '@/components/ui/markdown-viewer'
|
||||
import { Loader2, ArrowLeft, Calendar } from 'lucide-react'
|
||||
import { Loader2, ArrowLeft, Clock, Calendar } from 'lucide-react'
|
||||
|
||||
export default function BlogPost() {
|
||||
const { slug } = useParams()
|
||||
@@ -27,46 +27,65 @@ export default function BlogPost() {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (loading) return <div className="flex justify-center py-20"><Loader2 className="animate-spin" /></div>
|
||||
if (loading) return <div className="flex justify-center items-center h-[50vh]"><Loader2 className="animate-spin text-white" /></div>
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-20 text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Post not found</h1>
|
||||
<Link to="/blog" className="text-blue-600 hover:underline">← Back to Blog</Link>
|
||||
<div className="container mx-auto px-4 py-32 text-center">
|
||||
<h1 className="text-2xl font-bold mb-4 text-white">Post not found</h1>
|
||||
<Link to="/blog" className="text-muted-foreground hover:text-white transition-colors">← Back to Blog</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="container mx-auto px-4 py-12 max-w-3xl">
|
||||
<Link to="/blog" className="inline-flex items-center gap-2 text-sm text-slate-500 hover:text-blue-600 mb-8 transition-colors">
|
||||
<ArrowLeft size={16} /> Back to Blog
|
||||
</Link>
|
||||
<article className="min-h-screen pb-20">
|
||||
{/* Header */}
|
||||
<header className="relative py-20 md:py-32 container mx-auto px-4 max-w-4xl text-center">
|
||||
<div className="absolute inset-0 pointer-events-none bg-gradient-to-b from-blue-500/5 to-transparent blur-3xl" />
|
||||
|
||||
<header className="mb-8 items-center text-center">
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-slate-500 mb-4">
|
||||
<Calendar size={14} />
|
||||
{new Date(post.created_at).toLocaleDateString()}
|
||||
<span>•</span>
|
||||
<div className="flex gap-2">
|
||||
{post.tags?.map((tag: string) => (
|
||||
<span key={tag} className="text-blue-500 font-medium">#{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300">
|
||||
{post.title}
|
||||
</h1>
|
||||
{post.cover_image && (
|
||||
<div className="w-full h-[300px] md:h-[400px] rounded-2xl overflow-hidden shadow-lg mb-8">
|
||||
<img src={post.cover_image} alt={post.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<Link to="/blog" className="relative inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-white mb-8 transition-colors">
|
||||
<ArrowLeft size={16} /> Back to Blog
|
||||
</Link>
|
||||
|
||||
<h1 className="relative text-4xl md:text-6xl font-bold mb-8 leading-tight tracking-tight text-white">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<div className="relative flex flex-wrap items-center justify-center gap-6 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={14} />
|
||||
{new Date(post.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||
</div>
|
||||
{post.tags?.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
{post.tags.map((tag: string) => (
|
||||
<span key={tag} className="px-2 py-1 rounded-md bg-white/5 border border-white/5 text-xs font-mono text-white/80">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="bg-white dark:bg-slate-950">
|
||||
<MarkdownViewer content={post.content || ''} />
|
||||
{/* Hero Image */}
|
||||
{post.cover_image && (
|
||||
<div className="container mx-auto px-4 max-w-5xl mb-16">
|
||||
<div className="relative aspect-video rounded-2xl overflow-hidden border border-white/10 shadow-2xl">
|
||||
<img src={post.cover_image} alt={post.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent opacity-20" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<div className="p-8 md:p-12 rounded-3xl bg-white/5 border border-white/10 backdrop-blur-sm shadow-xl">
|
||||
<div className="prose prose-invert prose-lg max-w-none">
|
||||
<MarkdownViewer content={post.content || ''} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowRight, Code, Terminal, Zap } from 'lucide-react'
|
||||
import { ArrowRight, Sparkles } from 'lucide-react'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { ProjectCard } from '@/components/ui/project-card'
|
||||
import { motion } from 'framer-motion'
|
||||
@@ -27,26 +27,34 @@ export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{/* Hero Section */}
|
||||
<section className="relative h-[80vh] flex items-center justify-center overflow-hidden bg-slate-50 dark:bg-slate-950">
|
||||
<div className="absolute inset-0 bg-grid-slate-200/50 dark:bg-grid-slate-800/50 [mask-image:linear-gradient(0deg,white,rgba(255,255,255,0.6))] dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))]" />
|
||||
<section className="relative min-h-[90vh] flex items-center justify-center overflow-hidden">
|
||||
{/* Background Spotlight */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[500px] bg-white/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
|
||||
<div className="container px-4 text-center z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-cyan-500">
|
||||
Build. Deploy. Scale.
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs font-medium text-muted-foreground mb-8">
|
||||
<Sparkles size={12} className="text-yellow-200" />
|
||||
<span>Available for freelance work</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-6xl md:text-8xl font-bold mb-8 tracking-tight bg-clip-text text-transparent bg-gradient-to-b from-white to-white/50">
|
||||
Crafting Digital <br/> Experiences.
|
||||
</h1>
|
||||
<p className="text-xl text-slate-600 dark:text-slate-300 max-w-2xl mx-auto mb-8">
|
||||
Full-stack developer specializing in modern web technologies, serverless architectures, and high-performance applications.
|
||||
|
||||
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 leading-relaxed">
|
||||
Full-stack developer focused on building high-performance, accessible, and beautiful web applications using modern technologies.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
<Link to="/projects" className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors">
|
||||
<Link to="/projects" className="px-8 py-3 bg-white text-black rounded-full font-medium hover:bg-gray-200 transition-colors">
|
||||
View Work
|
||||
</Link>
|
||||
<Link to="/blog" className="px-6 py-3 border border-slate-300 dark:border-slate-700 rounded-lg font-medium hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
|
||||
<Link to="/blog" className="px-8 py-3 bg-white/5 border border-white/10 rounded-full font-medium hover:bg-white/10 transition-colors backdrop-blur-sm">
|
||||
Read Blog
|
||||
</Link>
|
||||
</div>
|
||||
@@ -54,65 +62,55 @@ export default function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Services/Tech Stack */}
|
||||
<section className="py-20 bg-white dark:bg-slate-900 border-y border-slate-100 dark:border-slate-800">
|
||||
<div className="container px-4 grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
{[
|
||||
{ icon: Code, title: "Frontend Dev", desc: "React, TypeScript, Tailwind CSS, Next.js" },
|
||||
{ icon: Terminal, title: "Backend Systems", desc: "Node.js, Supabase, PostgreSQL, APIs" },
|
||||
{ icon: Zap, title: "Performance", desc: "Serverless (Cloudflare), Optimization, SEO" }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="p-6 rounded-2xl bg-slate-50 dark:bg-slate-800/50">
|
||||
<item.icon className="mx-auto mb-4 text-blue-500" size={32} />
|
||||
<h3 className="text-xl font-bold mb-2">{item.title}</h3>
|
||||
<p className="text-slate-600 dark:text-slate-400">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Recent Projects */}
|
||||
<section className="py-24 container px-4 mx-auto">
|
||||
<div className="flex justify-between items-end mb-12">
|
||||
<section className="py-32 container px-4 mx-auto">
|
||||
<div className="flex justify-between items-end mb-16">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-2">Featured Projects</h2>
|
||||
<p className="text-slate-500">Some of my latest work.</p>
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight">Featured Projects</h2>
|
||||
<p className="text-muted-foreground">Selected works I've shipped recently.</p>
|
||||
</div>
|
||||
<Link to="/projects" className="text-blue-600 hover:underline flex items-center gap-1">
|
||||
<Link to="/projects" className="text-sm font-medium hover:text-white text-muted-foreground transition-colors flex items-center gap-2">
|
||||
View All <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{data.projects.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
<div key={project.id} className="h-[400px]">
|
||||
<ProjectCard project={project} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Recent Posts */}
|
||||
<section className="py-24 bg-slate-50 dark:bg-slate-900/50">
|
||||
<div className="container px-4 mx-auto">
|
||||
<div className="flex justify-between items-end mb-12">
|
||||
<section className="py-32 border-t border-white/5">
|
||||
<div className="container px-4 mx-auto max-w-4xl">
|
||||
<div className="flex justify-between items-end mb-16">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-2">Latest Articles</h2>
|
||||
<p className="text-slate-500">Thoughts on technology and coding.</p>
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight">Recent Thoughts</h2>
|
||||
<p className="text-muted-foreground">Writing about code, design, and my journey.</p>
|
||||
</div>
|
||||
<Link to="/blog" className="text-blue-600 hover:underline flex items-center gap-1">
|
||||
Read Blog <ArrowRight size={16} />
|
||||
<Link to="/blog" className="text-sm font-medium hover:text-white text-muted-foreground transition-colors flex items-center gap-2">
|
||||
All Posts <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
|
||||
<div className="space-y-4">
|
||||
{data.posts.map((post) => (
|
||||
<Link to={`/blog/${post.slug}`} key={post.id} className="block group">
|
||||
<article className="flex justify-between items-start p-6 bg-white dark:bg-slate-900 rounded-xl border border-slate-100 dark:border-slate-800 hover:shadow-md transition-shadow">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2 group-hover:text-blue-600 transition-colors">
|
||||
<article className="flex flex-col md:flex-row md:items-center justify-between p-6 bg-white/5 hover:bg-white/10 border border-white/10 rounded-2xl transition-all duration-300">
|
||||
<div className="mb-4 md:mb-0">
|
||||
<h3 className="text-xl font-semibold mb-2 group-hover:text-white transition-colors">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-400 line-clamp-2 mb-2">
|
||||
<p className="text-muted-foreground line-clamp-1 text-sm">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
<span className="text-sm text-slate-500">{new Date(post.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs font-mono text-muted-foreground min-w-fit">
|
||||
<span>{new Date(post.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</span>
|
||||
<ArrowRight size={14} className="opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
|
||||
@@ -26,29 +26,31 @@ export default function Projects() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[50vh]">
|
||||
<Loader2 className="animate-spin text-blue-500" size={32} />
|
||||
<Loader2 className="animate-spin text-white" size={32} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold mb-4">My Portfolio</h1>
|
||||
<p className="text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
|
||||
Showcase of my detailed projects and technical case studies.
|
||||
<div className="container mx-auto px-4 py-20">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl font-bold mb-4 tracking-tight bg-clip-text text-transparent bg-gradient-to-b from-white to-white/50">My Work</h1>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
A collection of projects exploring web development, design, and new technologies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{projects.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
<div key={project.id} className="h-[420px]">
|
||||
<ProjectCard project={project} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{projects.length === 0 && (
|
||||
<div className="text-center text-slate-500 py-12">
|
||||
No projects found yet.
|
||||
<div className="text-center text-muted-foreground py-20 bg-white/5 rounded-2xl border border-white/5">
|
||||
<p>No projects found yet. Check back soon!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -60,5 +60,8 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography"),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user