feat: 初始化项目并添加多个功能组件
style: 调整UI组件样式和布局 docs: 更新README和添加文档内容 chore: 添加依赖项和配置文件 fix: 修复一些小问题和优化代码 perf: 优化性能相关代码 refactor: 重构部分组件结构 test: 添加测试相关文件 build: 更新构建配置 ci: 添加CI配置文件
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.content-collections
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Dillion Verma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
47
README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
<div align="center">
|
||||
<img alt="Portfolio" src="https://github.com/dillionverma/portfolio/assets/16860528/57ffca81-3f0a-4425-b31d-094f61725455" width="90%">
|
||||
</div>
|
||||
|
||||
# Portfolio [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdillionverma%2Fportfolio)
|
||||
|
||||
Built with next.js, [shadcn/ui](https://ui.shadcn.com/), and [magic ui](https://magicui.design/), deployed on Vercel.
|
||||
|
||||
# Features
|
||||
|
||||
- Setup only takes a few minutes by editing the [single config file](./src/data/resume.tsx)
|
||||
- Built using Next.js 14, React, Typescript, Shadcn/UI, TailwindCSS, Framer Motion, Magic UI
|
||||
- Includes a blog
|
||||
- Responsive for different devices
|
||||
- Optimized for Next.js and Vercel
|
||||
|
||||
# Getting Started Locally
|
||||
|
||||
1. Clone this repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dillionverma/portfolio
|
||||
```
|
||||
|
||||
2. Move to the cloned directory
|
||||
|
||||
```bash
|
||||
cd portfolio
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. Start the local Server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
5. Open the [Config file](./src/data/resume.tsx) and make changes
|
||||
|
||||
# License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/dillionverma/portfolio/blob/main/LICENSE.md).
|
||||
24
components.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@svgl": "https://svgl.app/r/{name}.json"
|
||||
}
|
||||
}
|
||||
34
content-collections.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineCollection, defineConfig } from "@content-collections/core";
|
||||
import { compileMDX } from "@content-collections/mdx";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { z } from "zod";
|
||||
import { remarkCodeMeta } from "./src/lib/remark-code-meta";
|
||||
|
||||
const posts = defineCollection({
|
||||
name: "posts",
|
||||
directory: "content",
|
||||
include: "**/*.mdx",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
publishedAt: z.string(),
|
||||
updatedAt: z.string().optional(),
|
||||
author: z.string().optional(),
|
||||
summary: z.string(),
|
||||
image: z.string().optional(),
|
||||
content: z.string(),
|
||||
}),
|
||||
transform: async (document, context) => {
|
||||
const mdx = await compileMDX(context, document, {
|
||||
remarkPlugins: [remarkGfm, remarkCodeMeta],
|
||||
});
|
||||
return {
|
||||
...document,
|
||||
mdx,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
collections: [posts],
|
||||
});
|
||||
|
||||
33
content/api-design-principles.mdx
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "REST API Design Principles That Stand the Test of Time"
|
||||
publishedAt: "2024-12-12"
|
||||
updatedAt: "2024-12-12"
|
||||
author: "John Doe"
|
||||
summary: "Learn how to design APIs that developers love to use and are easy to maintain."
|
||||
image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
# REST API Design Principles That Stand the Test of Time
|
||||
|
||||
Great APIs feel boring in the best way: predictable, consistent, and easy to reason about. When the surface area is simple, teams ship faster and clients break less often.
|
||||
|
||||
This is a lightweight checklist you can keep in mind while designing new endpoints or reviewing an existing API.
|
||||
|
||||
## Core principles
|
||||
|
||||
- Use clear, consistent **resource names** (think nouns).
|
||||
- Keep behavior aligned with **HTTP semantics** (read vs write).
|
||||
- Return **consistent response shapes** so clients don’t guess.
|
||||
- Prefer **sane defaults** with optional query parameters for filtering/sorting.
|
||||
|
||||
## A simple checklist
|
||||
|
||||
1. Pick stable resource paths (plural nouns are a common convention).
|
||||
2. Use a small set of status codes consistently.
|
||||
3. Document pagination and what “next/previous” means.
|
||||
4. Be explicit about authentication and authorization requirements.
|
||||
5. Add brief examples in docs for the “happy path” and common errors.
|
||||
|
||||
## Wrap-up
|
||||
|
||||
If you optimize for consistency first, your API will be easier to document, easier to test, and easier for others to adopt.
|
||||
146
content/building-design-systems.mdx
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: "Building Scalable Design Systems with React and Tailwind"
|
||||
publishedAt: "2024-12-01"
|
||||
updatedAt: "2024-12-01"
|
||||
author: "John Doe"
|
||||
summary: "A comprehensive guide to creating maintainable design systems that scale with your team and product."
|
||||
image: "https://images.unsplash.com/photo-1558655146-9f40138edfeb?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
Design systems are the backbone of consistent user interfaces. Here's how to build one that scales.
|
||||
|
||||
## Why Design Systems Matter
|
||||
|
||||
A well-crafted design system provides:
|
||||
|
||||
- **Consistency** across all products
|
||||
- **Faster development** with reusable components
|
||||
- **Better collaboration** between designers and developers
|
||||
- **Reduced technical debt** over time
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Start with Tokens
|
||||
|
||||
Design tokens are the atomic values of your system:
|
||||
|
||||
```typescript title="tokens.ts"
|
||||
export const tokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
500: '#3b82f6',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
neutral: {
|
||||
0: '#ffffff',
|
||||
100: '#f5f5f5',
|
||||
900: '#171717',
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
xs: '0.25rem',
|
||||
sm: '0.5rem',
|
||||
md: '1rem',
|
||||
lg: '1.5rem',
|
||||
xl: '2rem',
|
||||
},
|
||||
radii: {
|
||||
sm: '0.25rem',
|
||||
md: '0.5rem',
|
||||
lg: '1rem',
|
||||
full: '9999px',
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 2. Build Primitive Components
|
||||
|
||||
Start with the basics:
|
||||
|
||||
```tsx title="Button.tsx"
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-primary text-white hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "h-8 px-3 text-sm",
|
||||
md: "h-10 px-4",
|
||||
lg: "h-12 px-6 text-lg",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
export function Button({ variant, size, className, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button className={buttonVariants({ variant, size, className })} {...props} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Component Composition
|
||||
|
||||
Build complex components from primitives:
|
||||
|
||||
| Level | Examples | Purpose |
|
||||
|-------|----------|---------|
|
||||
| Tokens | Colors, spacing, typography | Foundation |
|
||||
| Primitives | Button, Input, Badge | Building blocks |
|
||||
| Patterns | Card, Modal, Dropdown | Common UI patterns |
|
||||
| Templates | PageHeader, Sidebar | Layout structures |
|
||||
|
||||
## Documentation is Key
|
||||
|
||||
> "A design system without documentation is just a component library."
|
||||
|
||||
Every component should include:
|
||||
|
||||
1. **Usage examples** - Show common use cases
|
||||
2. **Props documentation** - Explain all options
|
||||
3. **Accessibility notes** - ARIA labels, keyboard nav
|
||||
4. **Do's and Don'ts** - Guide proper usage
|
||||
|
||||
## Versioning Strategy
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"name": "@company/design-system",
|
||||
"version": "2.1.0",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"tailwindcss": "^3.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use semantic versioning:
|
||||
- **Major**: Breaking changes
|
||||
- **Minor**: New features (backward compatible)
|
||||
- **Patch**: Bug fixes
|
||||
|
||||
## Conclusion
|
||||
|
||||
Building a design system is an investment that pays dividends in:
|
||||
|
||||
- Developer productivity
|
||||
- Design consistency
|
||||
- User experience
|
||||
- Team collaboration
|
||||
|
||||
Start small, iterate often, and document everything.
|
||||
38
content/git-workflow-guide.mdx
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: "Git Workflow Guide: From Chaos to Clarity"
|
||||
publishedAt: "2024-12-10"
|
||||
updatedAt: "2024-12-10"
|
||||
author: "John Doe"
|
||||
summary: "Master Git workflows that keep your team productive and your codebase healthy."
|
||||
image: "https://images.unsplash.com/photo-1556075798-4825dfaaf498?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
# Git Workflow Guide: From Chaos to Clarity
|
||||
|
||||
Git is powerful, but teams usually struggle because they don’t agree on a few basics. A simple workflow keeps history readable, reviews focused, and releases less stressful.
|
||||
|
||||
This is a lightweight guide you can adopt in a day and iterate on later.
|
||||
|
||||
## A simple team workflow
|
||||
|
||||
- Branch from `main` for every change.
|
||||
- Keep branches small and short-lived.
|
||||
- Open a pull request early and ask for review.
|
||||
- Merge back to `main` once tests pass.
|
||||
|
||||
## Commit message tips
|
||||
|
||||
- Start with a clear verb (“add”, “fix”, “remove”, “refactor”).
|
||||
- Keep the first line short and specific.
|
||||
- Prefer multiple small commits over one giant “WIP”.
|
||||
|
||||
## PR checklist
|
||||
|
||||
1. Explain what changed and why.
|
||||
2. Link the issue or describe the user impact.
|
||||
3. Add tests (or explain why not).
|
||||
4. Keep the diff small enough to review quickly.
|
||||
|
||||
## Wrap-up
|
||||
|
||||
The goal isn’t “perfect Git” — it’s fewer surprises and faster collaboration. Start simple, write down the rules, and improve them as your team grows.
|
||||
56
content/nextjs-performance-tips.mdx
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "10 Next.js Performance Tips for Production Apps"
|
||||
publishedAt: "2024-12-05"
|
||||
updatedAt: "2024-12-05"
|
||||
author: "John Doe"
|
||||
summary: "Practical optimization techniques to make your Next.js applications blazing fast in production."
|
||||
image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
Performance isn't optional—it's a feature. Here are battle-tested tips for optimizing Next.js apps.
|
||||
|
||||
# 2. Optimize Images
|
||||
|
||||
Always use `next/image`
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero image"
|
||||
width={1200}
|
||||
height={600}
|
||||
priority // Load immediately for LCP
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 4. Implement Proper Caching
|
||||
|
||||
| Strategy | Use Case | TTL |
|
||||
|----------|----------|-----|
|
||||
| `force-cache` | Static data | Forever |
|
||||
| `revalidate: 3600` | Semi-static | 1 hour |
|
||||
| `revalidate: 60` | Frequently updated | 1 minute |
|
||||
| `no-store` | Real-time data | Never cache |
|
||||
|
||||
## 5. Minimize Client Components
|
||||
|
||||
Every `'use client'` directive adds to your JavaScript bundle. Keep client components small and focused:
|
||||
|
||||
## 6. Bundle Analysis
|
||||
|
||||
```bash title="terminal"
|
||||
# Install analyzer
|
||||
npm install @next/bundle-analyzer
|
||||
|
||||
# Run analysis
|
||||
ANALYZE=true npm run build
|
||||
```
|
||||
43
content/remote-work-productivity.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "Mastering Remote Work: Productivity Tips from a Digital Nomad"
|
||||
publishedAt: "2024-11-25"
|
||||
updatedAt: "2024-11-25"
|
||||
author: "John Doe"
|
||||
summary: "Practical strategies for staying productive, focused, and balanced while working remotely, based on years of experience."
|
||||
image: "https://images.unsplash.com/photo-1521791136064-7986c2920216?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
# Mastering Remote Work: Productivity Tips
|
||||
|
||||
Remote work is less about “working from anywhere” and more about building routines that protect your focus and your energy. A few small defaults go a long way.
|
||||
|
||||
> Remote work works best when your day has clear starts, clear stops, and fewer context switches.
|
||||
|
||||
## Common challenges
|
||||
|
||||
- Home distractions
|
||||
- Blurry work/life boundaries
|
||||
- Fewer casual social touchpoints
|
||||
- Async communication gaps
|
||||
|
||||
## Daily routine checklist (example)
|
||||
|
||||
| Habit | Why it helps
|
||||
|------|--------------
|
||||
| Dedicated workspace | Signals "work mode"
|
||||
| Plan top 3 tasks | Reduces overwhelm
|
||||
| Deep work block | Protects focus
|
||||
| Walk / stretch break | Prevents burnout
|
||||
| Shutdown ritual | Creates a clear end
|
||||
|
||||
## Tools (keep it minimal)
|
||||
|
||||
- [x] Pick one task tracker (e.g. Linear / GitHub Issues)
|
||||
- [ ] Pick one notes app (e.g. Notion / Obsidian)
|
||||
- [ ] Keep a single calendar (e.g. Google Calendar)
|
||||
- [ ] Turn on Focus / Do Not Disturb during deep work
|
||||
- [ ] Prefer async updates; use chat for blockers only
|
||||
|
||||
## Wrap-up
|
||||
|
||||
Start with one routine change (workspace + “top 3 tasks”) and one boundary (a daily shutdown time). Once those stick, iterate.
|
||||
36
content/testing-react-apps.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "Testing React Applications: A Practical Guide"
|
||||
publishedAt: "2024-12-14"
|
||||
updatedAt: "2024-12-14"
|
||||
author: "John Doe"
|
||||
summary: "From unit tests to E2E—learn how to build confidence in your React applications with comprehensive testing."
|
||||
image: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
# Testing React Applications: A Practical Guide
|
||||
|
||||
Tests are your safety net — they let you refactor with confidence and ship changes without fear. The goal is not “as many tests as possible”; it’s <mark>confidence in the critical paths</mark>.
|
||||
|
||||
If your test suite feels slow or fragile, it usually means you’re testing the wrong thing. In practice, you can ignore a lot of “perfect coverage” advice and focus on a few high-signal habits.
|
||||
|
||||
## What to test (a simple rule)
|
||||
|
||||
- **Unit tests**: small, fast checks for logic and pure functions.
|
||||
- **Integration tests**: components + data + user flows (most value per test).
|
||||
- **E2E tests**: a few happy-path checks across the whole app.
|
||||
|
||||
## High-signal testing habits
|
||||
|
||||
- Prefer user-facing assertions (what the user sees/does).
|
||||
- Use accessible queries first (roles, labels).
|
||||
- Mock at the boundary (network) instead of mocking implementation details.
|
||||
|
||||
## Things to avoid
|
||||
|
||||
- ~~Testing internal component state~~ when behavior is what matters.
|
||||
- ~~Sprinkling test IDs everywhere~~ as a first choice.
|
||||
- Overusing snapshots that fail for harmless UI changes.
|
||||
|
||||
## Wrap-up
|
||||
|
||||
Start small: one integration test for your most important flow, then add unit tests for tricky logic. Over time, your tests become living documentation that helps you move faster.
|
||||
41
content/typescript-best-practices.mdx
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "TypeScript Best Practices for Clean, Maintainable Code"
|
||||
publishedAt: "2024-12-08"
|
||||
updatedAt: "2024-12-08"
|
||||
author: "John Doe"
|
||||
summary: "Essential TypeScript patterns and practices that will make your codebase more robust and easier to maintain."
|
||||
image: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800&h=192&fit=crop"
|
||||
---
|
||||
|
||||
# TypeScript Best Practices for Clean, Maintainable Code
|
||||
|
||||
TypeScript shines when it helps you model reality — not when it forces you to fight types all day. A few small defaults can make a codebase feel <mark>calmer, safer, and easier to refactor</mark>.
|
||||
|
||||
<MediaContainer
|
||||
src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=1200&fit=crop"
|
||||
alt="TypeScript code on screen"
|
||||
/>
|
||||
|
||||
## Practical rules of thumb
|
||||
|
||||
- Turn on strictness and fix the sharp edges early.
|
||||
- Prefer readable types over clever types.
|
||||
- Use unions for “one of these”, interfaces for “shape of this”.
|
||||
- Avoid `any` as a shortcut; it becomes ~~future debt~~ fast.
|
||||
|
||||
## One pattern worth memorizing
|
||||
|
||||
```ts
|
||||
type Result<T> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: string };
|
||||
|
||||
export function parseNumber(input: string): Result<number> {
|
||||
const n = Number(input);
|
||||
return Number.isFinite(n) ? { ok: true, value: n } : { ok: false, error: "Not a number" };
|
||||
}
|
||||
```
|
||||
|
||||
## Wrap-up
|
||||
|
||||
The best TypeScript code reads like good documentation: clear names, predictable shapes, and errors that point you to the fix.
|
||||
16
eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
'.next/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'next-env.d.ts',
|
||||
]),
|
||||
])
|
||||
|
||||
export default eslintConfig
|
||||
34
next.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { withContentCollections } from "@content-collections/next";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
{
|
||||
key: "X-Frame-Options",
|
||||
value: "DENY",
|
||||
},
|
||||
{
|
||||
key: "Referrer-Policy",
|
||||
value: "strict-origin-when-cross-origin",
|
||||
},
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
value: "camera=(), microphone=(), geolocation=()",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
// withContentCollections must be the outermost plugin
|
||||
export default withContentCollections(nextConfig);
|
||||
55
package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "porfolio",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@content-collections/mdx": "^0.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"content-collections": "^0.2.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.23.27",
|
||||
"next": "16.1.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^3.20.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@content-collections/core": "^0.13.1",
|
||||
"@content-collections/next": "^0.2.10",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"postcss": "^8.5.6",
|
||||
"typescript": "^5.9.3",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
7664
pnpm-lock.yaml
generated
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/atomic.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
public/buildspace.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/fonts/CabinetGrotesk-Medium.ttf
Normal file
BIN
public/fonts/ClashDisplay-Semibold.ttf
Normal file
BIN
public/ib.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
public/laurier.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
1
public/lime.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="800" width="1200" viewBox="-5.805105 -9.675175 50.31091 58.05105"><g fill-rule="evenodd" fill="#0D0"><path d="M16.8477 19.3501c0-1.3794 1.1245-2.504 2.504-2.504s2.5041 1.1171 2.5041 2.504c0 1.3795-1.1246 2.5041-2.5041 2.5041-1.3795 0-2.504-1.1171-2.504-2.5041zm6.7461-3.7108l6.0053-3.4712c1.1846-.6823 1.4095-2.3017.4498-3.2838-1.8818-1.9268-4.2809-3.3438-6.9799-4.0335-1.3345-.3374-2.6315.6523-2.6315 2.0317v6.9425c.0075 1.6119 1.7544 2.624 3.1563 1.8143zm-5.3365-1.8143V6.89c0-1.372-1.297-2.369-2.6315-2.0317-2.6916.6898-5.0907 2.1067-6.98 4.0335-.9596.9747-.7347 2.5941.4499 3.2838l6.0052 3.4712c1.4095.8022 3.1564-.2099 3.1564-1.8218zm6.4309 7.3471l6.0128 3.4712c1.1845.6823 2.699.0675 3.0663-1.2445a14.9416 14.9416 0 00.5548-4.041c0-1.402-.1949-2.7515-.5548-4.041-.3673-1.3196-1.8818-1.9268-3.0663-1.2446l-6.0128 3.4712c-1.402.8022-1.402 2.819 0 3.6287zm-9.5736 1.8892l-6.0053 3.4712c-1.1845.6822-1.4094 2.3016-.4498 3.2838 1.8818 1.9268 4.2809 3.3438 6.9799 4.0335 1.3345.3374 2.6316-.6523 2.6316-2.0318v-6.9424c-.015-1.6119-1.7619-2.6165-3.1564-1.8143zm-1.0939-5.5253l-6.0128-3.4713c-1.1846-.6822-2.699-.0674-3.0664 1.2446a14.9421 14.9421 0 00-.5548 4.041c0 1.402.195 2.7515.5548 4.041.3674 1.3195 1.8818 1.9268 3.0664 1.2445l6.0128-3.4712c1.3944-.8022 1.3944-2.8189 0-3.6286zm6.4266 7.3471v6.9425c0 1.372 1.297 2.3691 2.6315 2.0317 2.6915-.6897 5.0906-2.1067 6.9799-4.0335.9597-.9746.7347-2.594-.4498-3.2838l-6.0053-3.4712c-1.4095-.8097-3.1563.1949-3.1563 1.8143z" fill-rule="nonzero"/><path d="M19.3503 38.7007C8.6668 38.7007 0 30.0414 0 19.3503 0 8.6668 8.6668 0 19.3503 0c10.6911 0 19.3579 8.6668 19.3504 19.3503 0 10.6836-8.6593 19.3504-19.3504 19.3504zm17.1619-19.3505c0-9.4615-7.6996-17.1537-17.1611-17.1537-9.4615 0-17.1537 7.6922-17.1537 17.1537 0 9.4615 7.6922 17.1611 17.1537 17.1611 9.4615 0 17.1611-7.6996 17.1611-17.1611z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/me.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
public/mitremedia.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/nvidia.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
2
public/shopify.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-18 0 292 292" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-1.703-1.703-5.029-1.185-6.32-.805-.19.056-3.388 1.043-8.678 2.68-5.18-14.906-14.322-28.604-30.405-28.604-.444 0-.901.018-1.358.044C129.31 3.407 123.644.779 118.75.779c-37.465 0-55.364 46.835-60.976 70.635-14.558 4.511-24.9 7.718-26.221 8.133-8.126 2.549-8.383 2.805-9.45 10.462C21.3 95.806.038 260.235.038 260.235l165.678 31.042 89.77-19.42S223.973 58.8 223.775 57.34zM156.49 40.848l-14.019 4.339c.005-.988.01-1.96.01-3.023 0-9.264-1.286-16.723-3.349-22.636 8.287 1.04 13.806 10.469 17.358 21.32zm-27.638-19.483c2.304 5.773 3.802 14.058 3.802 25.238 0 .572-.005 1.095-.01 1.624-9.117 2.824-19.024 5.89-28.953 8.966 5.575-21.516 16.025-31.908 25.161-35.828zm-11.131-10.537c1.617 0 3.246.549 4.805 1.622-12.007 5.65-24.877 19.88-30.312 48.297l-22.886 7.088C75.694 46.16 90.81 10.828 117.72 10.828z" fill="#95BF46"/><path d="M221.237 54.983c-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-.637-.634-1.496-.959-2.394-1.099l-12.527 256.233 89.762-19.418S223.972 58.8 223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357" fill="#5E8E3E"/><path d="M135.242 104.585l-11.069 32.926s-9.698-5.176-21.586-5.176c-17.428 0-18.305 10.937-18.305 13.693 0 15.038 39.2 20.8 39.2 56.024 0 27.713-17.577 45.558-41.277 45.558-28.44 0-42.984-17.7-42.984-17.7l7.615-25.16s14.95 12.835 27.565 12.835c8.243 0 11.596-6.49 11.596-11.232 0-19.616-32.16-20.491-32.16-52.724 0-27.129 19.472-53.382 58.778-53.382 15.145 0 22.627 4.338 22.627 4.338" fill="#FFF"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
134
public/splunk.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/waterloo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
240
src/app/blog/[slug]/opengraph-image.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
|
||||
import { ImageResponse } from "next/og";
|
||||
import { allPosts } from "content-collections";
|
||||
import { DATA } from "@/data/resume";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const alt = "Blog Post";
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
|
||||
const getFontData = async () => {
|
||||
try {
|
||||
const [cabinetGrotesk, clashDisplay] = await Promise.all([
|
||||
fetch(
|
||||
new URL(
|
||||
"../../../../public/fonts/CabinetGrotesk-Medium.ttf",
|
||||
import.meta.url
|
||||
)
|
||||
).then((res) => res.arrayBuffer()),
|
||||
fetch(
|
||||
new URL(
|
||||
"../../../../public/fonts/ClashDisplay-Semibold.ttf",
|
||||
import.meta.url
|
||||
)
|
||||
).then((res) => res.arrayBuffer()),
|
||||
]);
|
||||
return { cabinetGrotesk, clashDisplay };
|
||||
} catch (error) {
|
||||
console.error("Failed to load fonts:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
outerWrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
},
|
||||
middleWrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
padding: "40px",
|
||||
},
|
||||
wrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#fafafa",
|
||||
position: "relative",
|
||||
padding: "40px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "12px",
|
||||
},
|
||||
imageSection: {
|
||||
position: "absolute",
|
||||
top: "40px",
|
||||
left: "40px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
zIndex: "2",
|
||||
},
|
||||
mainContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-end",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
},
|
||||
image: {
|
||||
width: "140px",
|
||||
height: "140px",
|
||||
borderRadius: "24px",
|
||||
border: "4px solid #e5e5e5",
|
||||
objectFit: "cover",
|
||||
},
|
||||
title: {
|
||||
fontFamily: "Clash Display",
|
||||
fontSize: "48px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "1.1",
|
||||
textAlign: "left",
|
||||
color: "#000000",
|
||||
marginBottom: "16px",
|
||||
letterSpacing: "-0.02em",
|
||||
maxWidth: "900px",
|
||||
},
|
||||
description: {
|
||||
fontSize: "20px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "1.5",
|
||||
textAlign: "left",
|
||||
maxWidth: "800px",
|
||||
color: "#404040",
|
||||
marginBottom: "16px",
|
||||
textWrap: "balance",
|
||||
},
|
||||
date: {
|
||||
fontSize: "16px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "1.5",
|
||||
textAlign: "left",
|
||||
color: "#666666",
|
||||
marginBottom: "32px",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
try {
|
||||
const fontData = await getFontData();
|
||||
const { slug } = await params;
|
||||
const post = allPosts.find((p) => p._meta.path.replace(/\.mdx$/, "") === slug);
|
||||
const imageUrl = DATA.avatarUrl
|
||||
? new URL(DATA.avatarUrl, DATA.url).toString()
|
||||
: undefined;
|
||||
|
||||
if (!post) {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div style={styles.outerWrapper}>
|
||||
<div style={styles.middleWrapper}>
|
||||
<div style={styles.wrapper}>
|
||||
{imageUrl && (
|
||||
<div style={styles.imageSection}>
|
||||
<img src={imageUrl} alt="Blog Post" style={styles.image} />
|
||||
</div>
|
||||
)}
|
||||
<div style={styles.mainContainer}>
|
||||
<div style={styles.title}>Post Not Found</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
fonts: fontData
|
||||
? [
|
||||
{
|
||||
name: "Clash Display",
|
||||
data: fontData.clashDisplay,
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const title = post.title;
|
||||
const description = post.summary || "";
|
||||
const publishedDate = post.publishedAt
|
||||
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
: "";
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div style={styles.outerWrapper}>
|
||||
<div style={styles.middleWrapper}>
|
||||
<div style={styles.wrapper}>
|
||||
{imageUrl && (
|
||||
<div style={styles.imageSection}>
|
||||
<img src={imageUrl} alt={title} style={styles.image} />
|
||||
</div>
|
||||
)}
|
||||
<div style={styles.mainContainer}>
|
||||
<div style={styles.title}>{title}</div>
|
||||
{description && (
|
||||
<div style={styles.description}>{description}</div>
|
||||
)}
|
||||
{publishedDate && <div style={styles.date}>{publishedDate}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
fonts: fontData
|
||||
? [
|
||||
{
|
||||
name: "Cabinet Grotesk",
|
||||
data: fontData.cabinetGrotesk,
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Cabinet Grotesk",
|
||||
data: fontData.cabinetGrotesk,
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Clash Display",
|
||||
data: fontData.clashDisplay,
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error generating OpenGraph image:", error);
|
||||
return new Response(
|
||||
`Failed to generate image: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
193
src/app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { allPosts } from "content-collections";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { DATA } from "@/data/resume";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { MDXContent } from "@content-collections/mdx/react";
|
||||
import { mdxComponents } from "@/mdx-components";
|
||||
import Link from "next/link";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
function getSortedPosts() {
|
||||
return [...allPosts].sort((a, b) => {
|
||||
if (new Date(a.publishedAt) > new Date(b.publishedAt)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return allPosts.map((post) => ({
|
||||
slug: post._meta.path.replace(/\.mdx$/, ""),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
}>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const { slug } = await params;
|
||||
const post = allPosts.find((p) => p._meta.path.replace(/\.mdx$/, "") === slug);
|
||||
|
||||
if (!post) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
publishedAt: publishedTime,
|
||||
summary: description,
|
||||
image,
|
||||
} = post;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: "article",
|
||||
publishedTime,
|
||||
url: `${DATA.url}/blog/${slug}`,
|
||||
...(image && {
|
||||
images: [
|
||||
{
|
||||
url: `${DATA.url}${image}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
...(image && {
|
||||
images: [`${DATA.url}${image}`],
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Blog({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
}>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const sortedPosts = getSortedPosts();
|
||||
const currentIndex = sortedPosts.findIndex(
|
||||
(p) => p._meta.path.replace(/\.mdx$/, "") === slug
|
||||
);
|
||||
const post = sortedPosts[currentIndex];
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const previousPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
|
||||
const nextPost = currentIndex < sortedPosts.length - 1 ? sortedPosts[currentIndex + 1] : null;
|
||||
|
||||
const getSlug = (post: (typeof sortedPosts)[0]) =>
|
||||
post._meta.path.replace(/\.mdx$/, "");
|
||||
|
||||
const jsonLdContent = JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: post.title,
|
||||
datePublished: post.publishedAt,
|
||||
dateModified: post.publishedAt,
|
||||
description: post.summary,
|
||||
image: post.image
|
||||
? `${DATA.url}${post.image}`
|
||||
: `${DATA.url}/blog/${slug}/opengraph-image`,
|
||||
url: `${DATA.url}/blog/${slug}`,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: DATA.name,
|
||||
},
|
||||
}).replace(/</g, "\\u003c");
|
||||
|
||||
return (
|
||||
<section id="blog">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonLdContent,
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-start gap-4 items-center">
|
||||
<Link href="/blog" className="text-sm text-muted-foreground hover:text-foreground transition-colors border border-border rounded-lg px-2 py-1 inline-flex items-center gap-1 mb-6 group" aria-label="Back to Blog">
|
||||
<ChevronLeft className="size-3 group-hover:-translate-x-px transition-transform" />
|
||||
Back to Blog
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="title font-semibold text-3xl md:text-4xl tracking-tighter leading-tight">
|
||||
{post.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatDate(post.publishedAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-6 flex w-full items-center">
|
||||
<div
|
||||
className="flex-1 h-px bg-border"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<article className="prose max-w-full text-pretty font-sans leading-relaxed text-muted-foreground dark:prose-invert">
|
||||
<MDXContent code={post.mdx} components={mdxComponents} />
|
||||
</article>
|
||||
|
||||
<nav className="mt-12 pt-8 max-w-2xl">
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
||||
{previousPost ? (
|
||||
<Link
|
||||
href={`/blog/${getSlug(previousPost)}`}
|
||||
className="group flex-1 flex flex-col gap-1 p-4 rounded-lg border border-border hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<ChevronLeft className="size-3" />
|
||||
Previous
|
||||
</span>
|
||||
<span className="text-sm font-medium group-hover:text-foreground transition-colors whitespace-normal wrap-break-word">
|
||||
{previousPost.title}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="hidden sm:block flex-1" />
|
||||
)}
|
||||
|
||||
{nextPost ? (
|
||||
<Link
|
||||
href={`/blog/${getSlug(nextPost)}`}
|
||||
className="group flex-1 flex flex-col gap-1 p-4 rounded-lg border border-border hover:bg-accent/50 transition-colors text-right"
|
||||
>
|
||||
<span className="flex items-center justify-end gap-1 text-xs text-muted-foreground">
|
||||
Next
|
||||
<ChevronRight className="size-3" />
|
||||
</span>
|
||||
<span className="text-sm font-medium group-hover:text-foreground transition-colors whitespace-normal wrap-break-word">
|
||||
{nextPost.title}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="hidden sm:block flex-1" />
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
174
src/app/blog/opengraph-image.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
|
||||
import { ImageResponse } from "next/og";
|
||||
import { DATA } from "@/data/resume";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const alt = "Blog";
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
|
||||
const getFontData = async () => {
|
||||
try {
|
||||
const [cabinetGrotesk, clashDisplay] = await Promise.all([
|
||||
fetch(
|
||||
new URL("../../../public/fonts/CabinetGrotesk-Medium.ttf", import.meta.url)
|
||||
).then((res) => res.arrayBuffer()),
|
||||
fetch(
|
||||
new URL("../../../public/fonts/ClashDisplay-Semibold.ttf", import.meta.url)
|
||||
).then((res) => res.arrayBuffer()),
|
||||
]);
|
||||
return { cabinetGrotesk, clashDisplay };
|
||||
} catch (error) {
|
||||
console.error("Failed to load fonts:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
outerWrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
},
|
||||
middleWrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
padding: "40px",
|
||||
},
|
||||
wrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#fafafa",
|
||||
position: "relative",
|
||||
padding: "40px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "12px",
|
||||
},
|
||||
imageSection: {
|
||||
position: "absolute",
|
||||
top: "40px",
|
||||
left: "40px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
zIndex: "2",
|
||||
},
|
||||
mainContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-end",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
},
|
||||
image: {
|
||||
width: "140px",
|
||||
height: "140px",
|
||||
borderRadius: "24px",
|
||||
border: "4px solid #e5e5e5",
|
||||
objectFit: "cover",
|
||||
},
|
||||
title: {
|
||||
fontFamily: "Clash Display",
|
||||
fontSize: "48px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "1.1",
|
||||
textAlign: "left",
|
||||
color: "#000000",
|
||||
marginBottom: "16px",
|
||||
letterSpacing: "-0.02em",
|
||||
maxWidth: "900px",
|
||||
},
|
||||
description: {
|
||||
fontSize: "20px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "1.5",
|
||||
textAlign: "left",
|
||||
maxWidth: "800px",
|
||||
color: "#404040",
|
||||
marginBottom: "32px",
|
||||
textWrap: "balance",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default async function Image() {
|
||||
try {
|
||||
const fontData = await getFontData();
|
||||
const title = "Blog";
|
||||
const description = "Thoughts on software development, life, and more.";
|
||||
const imageUrl = DATA.avatarUrl
|
||||
? new URL(DATA.avatarUrl, DATA.url).toString()
|
||||
: undefined;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div style={styles.outerWrapper}>
|
||||
<div style={styles.middleWrapper}>
|
||||
<div style={styles.wrapper}>
|
||||
{imageUrl && (
|
||||
<div style={styles.imageSection}>
|
||||
<img src={imageUrl} alt="Blog" style={styles.image} />
|
||||
</div>
|
||||
)}
|
||||
<div style={styles.mainContainer}>
|
||||
<div style={styles.title}>{title}</div>
|
||||
{description && (
|
||||
<div style={styles.description}>{description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
fonts: fontData
|
||||
? [
|
||||
{
|
||||
name: "Cabinet Grotesk",
|
||||
data: fontData.cabinetGrotesk,
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Cabinet Grotesk",
|
||||
data: fontData.cabinetGrotesk,
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Clash Display",
|
||||
data: fontData.clashDisplay,
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error generating OpenGraph image:", error);
|
||||
return new Response(
|
||||
`Failed to generate image: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
141
src/app/blog/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import BlurFade from "@/components/magicui/blur-fade";
|
||||
import { allPosts } from "content-collections";
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { paginate, normalizePage } from "@/lib/pagination";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog",
|
||||
description: "Thoughts on software development, life, and more.",
|
||||
openGraph: {
|
||||
title: "Blog",
|
||||
description: "Thoughts on software development, life, and more.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Blog",
|
||||
description: "Thoughts on software development, life, and more.",
|
||||
},
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
const BLUR_FADE_DELAY = 0.04;
|
||||
|
||||
export default async function BlogPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}) {
|
||||
const { page: pageParam } = await searchParams;
|
||||
|
||||
const posts = allPosts;
|
||||
const sortedPosts = [...posts].sort((a, b) => {
|
||||
if (new Date(a.publishedAt) > new Date(b.publishedAt)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(sortedPosts.length / PAGE_SIZE);
|
||||
const currentPage = normalizePage(pageParam, totalPages);
|
||||
const { items: paginatedPosts, pagination } = paginate(sortedPosts, {
|
||||
page: currentPage,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
|
||||
return (
|
||||
<section id="blog">
|
||||
<BlurFade delay={BLUR_FADE_DELAY}>
|
||||
<h1 className="text-2xl font-semibold tracking-tight mb-2">Blog <span className="ml-1 bg-card border border-border rounded-md px-2 py-1 text-muted-foreground text-sm">{sortedPosts.length} posts</span></h1>
|
||||
<p className="text-sm text-muted-foreground mb-8">
|
||||
My thoughts on software development, life, and more.
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
{paginatedPosts.length > 0 ? (
|
||||
<>
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 2}>
|
||||
<div className="flex flex-col gap-5">
|
||||
{paginatedPosts.map((post, id) => {
|
||||
const slug = post._meta.path.replace(/\.mdx$/, "");
|
||||
const indexNumber = (pagination.page - 1) * PAGE_SIZE + id + 1;
|
||||
return (
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 3 + id * 0.05} key={slug}>
|
||||
<Link
|
||||
className="flex items-start gap-x-2 group cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
href={`/blog/${slug}`}
|
||||
>
|
||||
<span className="text-xs font-mono tabular-nums font-medium mt-[5px]">
|
||||
{String(indexNumber).padStart(2, "0")}.
|
||||
</span>
|
||||
<div className="flex flex-col gap-y-2 flex-1">
|
||||
<p className="tracking-tight text-lg font-medium">
|
||||
<span className="group-hover:text-foreground transition-colors">
|
||||
{post.title}
|
||||
<ChevronRight
|
||||
className="ml-1 inline-block size-4 stroke-3 text-muted-foreground opacity-0 -translate-x-2 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0"
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{post.publishedAt}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</BlurFade>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 4}>
|
||||
<div className="flex gap-3 flex-row items-center justify-between mt-8">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Page {pagination.page} of {pagination.totalPages}
|
||||
</div>
|
||||
<div className="flex gap-2 sm:justify-end">
|
||||
{pagination.hasPreviousPage ? (
|
||||
<Link
|
||||
href={`/blog?page=${pagination.page - 1}`}
|
||||
className="h-8 w-fit px-2 flex items-center justify-center text-sm border border-border rounded-lg hover:bg-accent/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
) : (
|
||||
<span className="h-8 w-fit px-2 flex items-center justify-center text-sm border border-border rounded-lg opacity-50 cursor-not-allowed">
|
||||
Previous
|
||||
</span>
|
||||
)}
|
||||
{pagination.hasNextPage ? (
|
||||
<Link
|
||||
href={`/blog?page=${pagination.page + 1}`}
|
||||
className="h-8 w-fit px-2 flex items-center justify-center text-sm border border-border rounded-lg hover:bg-accent/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
) : (
|
||||
<span className="h-8 w-fit px-2 flex items-center justify-center text-sm border border-border rounded-lg opacity-50 cursor-not-allowed">
|
||||
Next
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 2}>
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 border border-border rounded-xl">
|
||||
<p className="text-muted-foreground text-center">
|
||||
No blog posts yet. Check back soon!
|
||||
</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
236
src/app/globals.css
Normal file
@@ -0,0 +1,236 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.18 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* Shiki dual theme support */
|
||||
.shiki,
|
||||
.shiki span {
|
||||
color: var(--shiki-light);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
@apply leading-relaxed bg-transparent! font-mono!;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
@apply m-0! mx-1! border rounded-md! px-1.5 py-0.5 bg-muted/60 dark:bg-muted/40 text-[13px]! font-mono text-foreground/90!;
|
||||
}
|
||||
|
||||
.prose code::before, .prose code::after {
|
||||
@apply content-none!;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
@apply text-2xl! font-semibold! tracking-tight! leading-tight!;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
@apply text-xl! font-semibold! tracking-tight! leading-tight!;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
@apply text-lg! font-semibold! tracking-tight! leading-tight!;
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
@apply text-base! font-medium! tracking-tight! leading-tight!;
|
||||
}
|
||||
|
||||
.prose h5 {
|
||||
@apply text-base! font-medium! tracking-tight! leading-tight!;
|
||||
}
|
||||
|
||||
.prose h6 {
|
||||
@apply text-base! font-medium! tracking-tight! leading-tight!;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
@apply text-primary underline underline-offset-4;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
@apply border-l-4 border-amber-500! pl-4 italic bg-muted/50 p-4 rounded-md rounded-l-none;
|
||||
}
|
||||
|
||||
.prose blockquote p {
|
||||
@apply m-0!;
|
||||
}
|
||||
|
||||
.prose blockquote p:first-child {
|
||||
@apply mt-0!;
|
||||
}
|
||||
|
||||
/* Task lists (remark-gfm): remove bullet marker so it renders like a pure checklist */
|
||||
.prose :where(ul.contains-task-list):not(:where([class~="not-prose"] *)) {
|
||||
@apply list-none! pl-0!;
|
||||
}
|
||||
|
||||
.prose :where(li.task-list-item):not(:where([class~="not-prose"] *)) {
|
||||
@apply pl-0!;
|
||||
}
|
||||
|
||||
.prose
|
||||
:where(li.task-list-item > input[type="checkbox"]):not(
|
||||
:where([class~="not-prose"] *)
|
||||
) {
|
||||
@apply mr-2 align-middle!;
|
||||
}
|
||||
|
||||
.prose :where(table):not(:where([class~="not-prose"]) *) {
|
||||
@apply w-full min-w-full;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.prose :where(th):not(:where([class~="not-prose"] *)),
|
||||
.prose :where(td):not(:where([class~="not-prose"] *)) {
|
||||
@apply px-6 py-4 align-middle m-0 whitespace-nowrap;
|
||||
}
|
||||
|
||||
.prose tr{
|
||||
@apply hover:bg-accent/50! transition-colors! cursor-pointer! h-10!;
|
||||
}
|
||||
|
||||
.prose :where(th):not(:where([class~="not-prose"] *)) {
|
||||
@apply text-left font-semibold text-foreground border-b border-border m-0! bg-muted/50 p-3! py-2!;
|
||||
}
|
||||
|
||||
.prose :where(td):not(:where([class~="not-prose"] *)) {
|
||||
@apply text-muted-foreground border-b border-border text-sm! p-2!;
|
||||
}
|
||||
|
||||
.prose :where(th:not(:last-child)):not(:where([class~="not-prose"] *)),
|
||||
.prose :where(td:not(:last-child)):not(:where([class~="not-prose"] *)) {
|
||||
@apply border-r border-border;
|
||||
}
|
||||
|
||||
.prose :where(tbody tr:last-child td):not(:where([class~="not-prose"] *)) {
|
||||
@apply border-b-0;
|
||||
}
|
||||
|
||||
}
|
||||
95
src/app/layout.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import Navbar from "@/components/navbar";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { DATA } from "@/data/resume";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { FlickeringGrid } from "@/components/magicui/flickering-grid";
|
||||
|
||||
const geist = Geist({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
variable: "--font-mono",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(DATA.url),
|
||||
title: {
|
||||
default: DATA.name,
|
||||
template: `%s | ${DATA.name}`,
|
||||
},
|
||||
description: DATA.description,
|
||||
openGraph: {
|
||||
title: `${DATA.name}`,
|
||||
description: DATA.description,
|
||||
url: DATA.url,
|
||||
siteName: `${DATA.name}`,
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
twitter: {
|
||||
title: `${DATA.name}`,
|
||||
card: "summary_large_image",
|
||||
},
|
||||
verification: {
|
||||
google: "",
|
||||
yandex: "",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased relative",
|
||||
geist.variable,
|
||||
geistMono.variable
|
||||
)}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div className="absolute inset-0 top-0 left-0 right-0 h-[100px] overflow-hidden z-0">
|
||||
<FlickeringGrid
|
||||
className="h-full w-full"
|
||||
squareSize={2}
|
||||
gridGap={2}
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black, transparent)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 max-w-2xl mx-auto py-12 pb-24 sm:py-24 px-6">
|
||||
{children}
|
||||
</div>
|
||||
<Navbar />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
35
src/app/not-found.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Link from "next/link";
|
||||
import { Home } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-12rem)] flex flex-col">
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="flex flex-col items-center text-center max-w-md relative">
|
||||
<h1 className="text-[200px] font-semibold font-mono bg-linear-to-b from-primary/30 to-secondary/10 text-transparent bg-clip-text absolute -top-40 left-1/2 -translate-x-1/2 mask-[linear-gradient(to_bottom,black,black_20%,transparent_80%)] tracking-tighter uppercase [-webkit-text-stroke:3px_hsl(var(--primary)/0.6)]">
|
||||
404
|
||||
</h1>
|
||||
<h2 className="text-4xl tracking-tight font-semibold text-foreground mb-2">
|
||||
Page Not Found
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-8 text-balance tracking-tight font-medium">
|
||||
The page you're looking for doesn't exist or may have been
|
||||
moved.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Link href="/">
|
||||
<Button variant="outline" className="gap-2 cursor-pointer">
|
||||
<Home className="h-4 w-4" />
|
||||
Go to Home
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
172
src/app/opengraph-image.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
import { ImageResponse } from "next/og";
|
||||
import { DATA } from "@/data/resume";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const alt = DATA.name;
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
|
||||
const getFontData = async () => {
|
||||
try {
|
||||
const [cabinetGrotesk, clashDisplay] = await Promise.all([
|
||||
fetch(
|
||||
new URL("../../public/fonts/CabinetGrotesk-Medium.ttf", import.meta.url)
|
||||
).then((res) => res.arrayBuffer()),
|
||||
fetch(
|
||||
new URL("../../public/fonts/ClashDisplay-Semibold.ttf", import.meta.url)
|
||||
).then((res) => res.arrayBuffer()),
|
||||
]);
|
||||
return { cabinetGrotesk, clashDisplay };
|
||||
} catch (error) {
|
||||
console.error("Failed to load fonts:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
outerWrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
},
|
||||
middleWrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
padding: "40px",
|
||||
},
|
||||
wrapper: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#fafafa",
|
||||
position: "relative",
|
||||
padding: "40px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "12px",
|
||||
},
|
||||
imageSection: {
|
||||
position: "absolute",
|
||||
top: "40px",
|
||||
left: "40px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
zIndex: "2",
|
||||
},
|
||||
mainContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-end",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
},
|
||||
image: {
|
||||
width: "140px",
|
||||
height: "140px",
|
||||
borderRadius: "24px",
|
||||
border: "4px solid #e5e5e5",
|
||||
objectFit: "cover",
|
||||
},
|
||||
title: {
|
||||
fontFamily: "Clash Display",
|
||||
fontSize: "48px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "1.1",
|
||||
textAlign: "left",
|
||||
color: "#000000",
|
||||
marginBottom: "16px",
|
||||
letterSpacing: "-0.02em",
|
||||
maxWidth: "900px",
|
||||
},
|
||||
description: {
|
||||
fontSize: "20px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "1.5",
|
||||
textAlign: "left",
|
||||
maxWidth: "800px",
|
||||
color: "#404040",
|
||||
marginBottom: "32px",
|
||||
textWrap: "balance",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default async function Image() {
|
||||
try {
|
||||
const fontData = await getFontData();
|
||||
const imageUrl = DATA.avatarUrl
|
||||
? new URL(DATA.avatarUrl, DATA.url).toString()
|
||||
: undefined;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div style={styles.outerWrapper}>
|
||||
<div style={styles.middleWrapper}>
|
||||
<div style={styles.wrapper}>
|
||||
{imageUrl && (
|
||||
<div style={styles.imageSection}>
|
||||
<img src={imageUrl} alt={DATA.name} style={styles.image} />
|
||||
</div>
|
||||
)}
|
||||
<div style={styles.mainContainer}>
|
||||
<div style={styles.title}>{DATA.name}</div>
|
||||
{DATA.description && (
|
||||
<div style={styles.description}>{DATA.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
fonts: fontData
|
||||
? [
|
||||
{
|
||||
name: "Cabinet Grotesk",
|
||||
data: fontData.cabinetGrotesk,
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Cabinet Grotesk",
|
||||
data: fontData.cabinetGrotesk,
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Clash Display",
|
||||
data: fontData.clashDisplay,
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error generating OpenGraph image:", error);
|
||||
return new Response(
|
||||
`Failed to generate image: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
150
src/app/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import BlurFade from "@/components/magicui/blur-fade";
|
||||
import BlurFadeText from "@/components/magicui/blur-fade-text";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { DATA } from "@/data/resume";
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
import ContactSection from "@/components/section/contact-section";
|
||||
import HackathonsSection from "@/components/section/hackathons-section";
|
||||
import ProjectsSection from "@/components/section/projects-section";
|
||||
import WorkSection from "@/components/section/work-section";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
const BLUR_FADE_DELAY = 0.04;
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main className="min-h-dvh flex flex-col gap-14 relative">
|
||||
<section id="hero">
|
||||
<div className="mx-auto w-full max-w-2xl space-y-8">
|
||||
<div className="gap-2 gap-y-6 flex flex-col md:flex-row justify-between">
|
||||
<div className="gap-2 flex flex-col order-2 md:order-1">
|
||||
<BlurFadeText
|
||||
delay={BLUR_FADE_DELAY}
|
||||
className="text-3xl font-semibold tracking-tighter sm:text-4xl lg:text-5xl"
|
||||
yOffset={8}
|
||||
text={`Hi, I'm ${DATA.name.split(" ")[0]}`}
|
||||
/>
|
||||
<BlurFadeText
|
||||
className="text-muted-foreground max-w-[600px] md:text-lg lg:text-xl"
|
||||
delay={BLUR_FADE_DELAY}
|
||||
text={DATA.description}
|
||||
/>
|
||||
</div>
|
||||
<BlurFade delay={BLUR_FADE_DELAY} className="order-1 md:order-2">
|
||||
<Avatar className="size-24 md:size-32 border rounded-full shadow-lg ring-4 ring-muted">
|
||||
<AvatarImage alt={DATA.name} src={DATA.avatarUrl} />
|
||||
<AvatarFallback>{DATA.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="about">
|
||||
<div className="flex min-h-0 flex-col gap-y-4">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 3}>
|
||||
<h2 className="text-xl font-bold">About</h2>
|
||||
</BlurFade>
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 4}>
|
||||
<div className="prose max-w-full text-pretty font-sans leading-relaxed text-muted-foreground dark:prose-invert">
|
||||
<Markdown>
|
||||
{DATA.summary}
|
||||
</Markdown>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
<section id="work">
|
||||
<div className="flex min-h-0 flex-col gap-y-6">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 5}>
|
||||
<h2 className="text-xl font-bold">Work Experience</h2>
|
||||
</BlurFade>
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 6}>
|
||||
<WorkSection />
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
<section id="education">
|
||||
<div className="flex min-h-0 flex-col gap-y-6">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 7}>
|
||||
<h2 className="text-xl font-bold">Education</h2>
|
||||
</BlurFade>
|
||||
<div className="flex flex-col gap-8">
|
||||
{DATA.education.map((education, index) => (
|
||||
<BlurFade
|
||||
key={education.school}
|
||||
delay={BLUR_FADE_DELAY * 8 + index * 0.05}
|
||||
>
|
||||
<Link
|
||||
href={education.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-x-3 justify-between group"
|
||||
>
|
||||
<div className="flex items-center gap-x-3 flex-1 min-w-0">
|
||||
{education.logoUrl ? (
|
||||
<img
|
||||
src={education.logoUrl}
|
||||
alt={education.school}
|
||||
className="size-8 md:size-10 p-1 border rounded-full shadow ring-2 ring-border overflow-hidden object-contain flex-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-8 md:size-10 p-1 border rounded-full shadow ring-2 ring-border bg-muted flex-none" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<div className="font-semibold leading-none flex items-center gap-2">
|
||||
{education.school}
|
||||
<ArrowUpRight className="h-3.5 w-3.5 text-muted-foreground opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200" aria-hidden />
|
||||
</div>
|
||||
<div className="font-sans text-sm text-muted-foreground">
|
||||
{education.degree}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs tabular-nums text-muted-foreground text-right flex-none">
|
||||
<span>
|
||||
{education.start} - {education.end}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="skills">
|
||||
<div className="flex min-h-0 flex-col gap-y-4">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 9}>
|
||||
<h2 className="text-xl font-bold">Skills</h2>
|
||||
</BlurFade>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DATA.skills.map((skill, id) => (
|
||||
<BlurFade key={skill.name} delay={BLUR_FADE_DELAY * 10 + id * 0.05}>
|
||||
<div className="border bg-background border-border ring-2 ring-border/20 rounded-xl h-8 w-fit px-4 flex items-center gap-2">
|
||||
{skill.icon && <skill.icon className="size-4 rounded overflow-hidden object-contain" />}
|
||||
<span className="text-foreground text-sm font-medium">{skill.name}</span>
|
||||
</div>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="projects">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 11}>
|
||||
<ProjectsSection />
|
||||
</BlurFade>
|
||||
</section>
|
||||
<section id="hackathons">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 13}>
|
||||
<HackathonsSection />
|
||||
</BlurFade>
|
||||
</section>
|
||||
<section id="contact">
|
||||
<BlurFade delay={BLUR_FADE_DELAY * 16}>
|
||||
<ContactSection />
|
||||
</BlurFade>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
229
src/components/icons.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import { GlobeIcon, MailIcon } from "lucide-react";
|
||||
|
||||
export type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
|
||||
export const Icons = {
|
||||
globe: (props: IconProps) => <GlobeIcon {...props} />,
|
||||
email: (props: IconProps) => <MailIcon {...props} />,
|
||||
linkedin: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<title>LinkedIn</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
x: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<title>X</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
youtube: (props: IconProps) => (
|
||||
<svg
|
||||
width="32px"
|
||||
height="32px"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>youtube</title>
|
||||
<path d="M29.41,9.26a3.5,3.5,0,0,0-2.47-2.47C24.76,6.2,16,6.2,16,6.2s-8.76,0-10.94.59A3.5,3.5,0,0,0,2.59,9.26,36.13,36.13,0,0,0,2,16a36.13,36.13,0,0,0,.59,6.74,3.5,3.5,0,0,0,2.47,2.47C7.24,25.8,16,25.8,16,25.8s8.76,0,10.94-.59a3.5,3.5,0,0,0,2.47-2.47A36.13,36.13,0,0,0,30,16,36.13,36.13,0,0,0,29.41,9.26ZM13.2,20.2V11.8L20.47,16Z" />
|
||||
</svg>
|
||||
),
|
||||
nextjs: (props: IconProps) => (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-8"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<title>Next.js</title>
|
||||
<path d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z" />
|
||||
</svg>
|
||||
),
|
||||
framermotion: (props: IconProps) => (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<title>Framer Motion</title>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12l-8 -8v16l16 -16v16l-4 -4" />
|
||||
<path d="M20 12l-8 8l-4 -4" />
|
||||
</svg>
|
||||
),
|
||||
tailwindcss: (props: IconProps) => (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-8"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<title>Tailwind CSS</title>
|
||||
<path d="m12.001 4.8c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624-1.176-1.194-2.537-2.576-5.512-2.576zm-6 7.2c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624-1.176-1.194-2.537-2.576-5.512-2.576z" />
|
||||
</svg>
|
||||
),
|
||||
typescript: (props: IconProps) => (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-8"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="m0 16v16h32v-32h-32zm25.786-1.276c.813.203 1.432.568 2.005 1.156.292.312.729.885.766 1.026.01.042-1.38.974-2.224 1.495-.031.021-.156-.109-.292-.313-.411-.599-.844-.859-1.505-.906-.969-.063-1.594.443-1.589 1.292-.005.208.042.417.135.599.214.443.615.708 1.854 1.245 2.292.984 3.271 1.635 3.88 2.557.682 1.031.833 2.677.375 3.906-.51 1.328-1.771 2.234-3.542 2.531-.547.099-1.849.083-2.438-.026-1.286-.229-2.505-.865-3.255-1.698-.297-.323-.87-1.172-.833-1.229.016-.021.146-.104.292-.188s.682-.396 1.188-.688l.922-.536.193.286c.271.411.859.974 1.214 1.161 1.021.542 2.422.464 3.115-.156.281-.234.438-.594.417-.958 0-.37-.047-.536-.24-.813-.25-.354-.755-.656-2.198-1.281-1.651-.714-2.365-1.151-3.01-1.854-.406-.464-.708-1.01-.88-1.599-.12-.453-.151-1.589-.057-2.042.339-1.599 1.547-2.708 3.281-3.036.563-.109 1.875-.068 2.427.068zm-7.51 1.339.01 1.307h-4.167v11.839h-2.948v-11.839h-4.161v-1.281c0-.714.016-1.307.036-1.323.016-.021 2.547-.031 5.62-.026l5.594.016z" />
|
||||
</svg>
|
||||
),
|
||||
react: (props: IconProps) => (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-8"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<title>React</title>
|
||||
<path d="m16 13.146c-1.573 0-2.854 1.281-2.854 2.854s1.281 2.854 2.854 2.854 2.854-1.281 2.854-2.854-1.281-2.854-2.854-2.854zm-7.99 8.526-.63-.156c-4.688-1.188-7.38-3.198-7.38-5.521s2.693-4.333 7.38-5.521l.63-.156.177.625c.474 1.635 1.083 3.229 1.818 4.771l.135.281-.135.286c-.734 1.536-1.344 3.13-1.818 4.771zm-.921-9.74c-3.563 1-5.75 2.536-5.75 4.063s2.188 3.057 5.75 4.063c.438-1.391.964-2.745 1.578-4.063-.615-1.318-1.141-2.672-1.578-4.063zm16.901 9.74-.177-.625c-.474-1.635-1.083-3.229-1.818-4.766l-.135-.286.135-.286c.734-1.536 1.344-3.13 1.818-4.771l.177-.62.63.156c4.688 1.188 7.38 3.198 7.38 5.521s-2.693 4.333-7.38 5.521zm-.657-5.677c.641 1.385 1.172 2.745 1.578 4.063 3.568-1.005 5.75-2.536 5.75-4.063s-2.188-3.057-5.75-4.063c-.438 1.385-.964 2.745-1.578 4.063zm-16.255-4.068-.177-.625c-1.318-4.646-.917-7.979 1.099-9.141 1.979-1.141 5.151.208 8.479 3.625l.453.464-.453.464c-1.182 1.229-2.26 2.552-3.229 3.958l-.182.255-.313.026c-1.703.135-3.391.406-5.047.813zm2.531-8.838c-.359 0-.677.073-.943.229-1.323.766-1.557 3.422-.646 7.005 1.422-.318 2.859-.542 4.313-.672.833-1.188 1.75-2.323 2.734-3.391-2.078-2.026-4.047-3.172-5.458-3.172zm12.787 27.145c-.005 0-.005 0 0 0-1.901 0-4.344-1.427-6.875-4.031l-.453-.464.453-.464c1.182-1.229 2.26-2.552 3.229-3.958l.177-.255.313-.031c1.703-.13 3.391-.401 5.052-.813l.63-.156.177.625c1.318 4.646.917 7.974-1.099 9.135-.49.281-1.042.422-1.604.411zm-5.464-4.505c2.078 2.026 4.047 3.172 5.458 3.172h.005c.354 0 .672-.078.938-.229 1.323-.766 1.563-3.422.646-7.005-1.422.318-2.865.542-4.313.667-.833 1.193-1.75 2.323-2.734 3.396zm7.99-13.802-.63-.161c-1.661-.406-3.349-.677-5.052-.813l-.313-.026-.177-.255c-.969-1.406-2.047-2.729-3.229-3.958l-.453-.464.453-.464c3.328-3.417 6.5-4.766 8.479-3.625 2.016 1.161 2.417 4.495 1.099 9.141zm-5.255-2.276c1.521.141 2.969.365 4.313.672.917-3.583.677-6.24-.646-7.005-1.318-.76-3.797.406-6.401 2.943.984 1.073 1.896 2.203 2.734 3.391zm-10.058 20.583c-.563.01-1.12-.13-1.609-.411-2.016-1.161-2.417-4.49-1.099-9.135l.177-.625.63.156c1.542.391 3.24.661 5.047.813l.313.031.177.255c.969 1.406 2.047 2.729 3.229 3.958l.453.464-.453.464c-2.526 2.604-4.969 4.031-6.865 4.031zm-1.588-8.567c-.917 3.583-.677 6.24.646 7.005 1.318.75 3.792-.406 6.401-2.943-.984-1.073-1.901-2.203-2.734-3.396-1.453-.125-2.891-.349-4.313-.667zm7.979.838c-1.099 0-2.224-.047-3.354-.141l-.313-.026-.182-.26c-.635-.917-1.24-1.859-1.797-2.828-.563-.969-1.078-1.958-1.557-2.969l-.135-.286.135-.286c.479-1.01.995-2 1.557-2.969.552-.953 1.156-1.906 1.797-2.828l.182-.26.313-.026c2.234-.188 4.479-.188 6.708 0l.313.026.182.26c1.276 1.833 2.401 3.776 3.354 5.797l.135.286-.135.286c-.953 2.021-2.073 3.964-3.354 5.797l-.182.26-.313.026c-1.125.094-2.255.141-3.354.141zm-2.927-1.448c1.969.151 3.885.151 5.859 0 1.099-1.609 2.078-3.302 2.927-5.063-.844-1.76-1.823-3.453-2.932-5.063-1.948-.151-3.906-.151-5.854 0-1.109 1.609-2.089 3.302-2.932 5.063.849 1.76 1.828 3.453 2.932 5.063z" />
|
||||
</svg>
|
||||
),
|
||||
github: (props: IconProps) => (
|
||||
<svg
|
||||
viewBox="0 0 438.549 438.549"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
notion: (props: IconProps) => (
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z"
|
||||
fill="#000"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
openai: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
),
|
||||
googleDrive: (props: IconProps) => (
|
||||
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
|
||||
fill="#0066da"
|
||||
/>
|
||||
<path
|
||||
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
|
||||
fill="#00ac47"
|
||||
/>
|
||||
<path
|
||||
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
|
||||
fill="#ea4335"
|
||||
/>
|
||||
<path
|
||||
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
|
||||
fill="#00832d"
|
||||
/>
|
||||
<path
|
||||
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
|
||||
fill="#2684fc"
|
||||
/>
|
||||
<path
|
||||
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
|
||||
fill="#ffba00"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
whatsapp: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 175.216 175.552"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="85.915"
|
||||
x2="86.535"
|
||||
y1="32.567"
|
||||
y2="137.092"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#57d163" />
|
||||
<stop offset="1" stopColor="#23b33a" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="a"
|
||||
width="1.115"
|
||||
height="1.114"
|
||||
x="-.057"
|
||||
y="-.057"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feGaussianBlur stdDeviation="3.531" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path
|
||||
fill="#b3b3b3"
|
||||
d="m54.532 138.45 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.523h.023c33.707 0 61.139-27.426 61.153-61.135.006-16.335-6.349-31.696-17.895-43.251A60.75 60.75 0 0 0 87.94 25.983c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.558zm-40.811 23.544L24.16 123.88c-6.438-11.154-9.825-23.808-9.821-36.772.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954zm0 0"
|
||||
filter="url(#a)"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m12.966 161.238 10.439-38.114a73.42 73.42 0 0 1-9.821-36.772c.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#linearGradient1780)"
|
||||
d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.559 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.524h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.929z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.313-6.179 22.558 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.517 31.126 8.523h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.928z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
d="M68.772 55.603c-1.378-3.061-2.828-3.123-4.137-3.176l-3.524-.043c-1.226 0-3.218.46-4.902 2.3s-6.435 6.287-6.435 15.332 6.588 17.785 7.506 19.013 12.718 20.381 31.405 27.75c15.529 6.124 18.689 4.906 22.061 4.6s10.877-4.447 12.408-8.74 1.532-7.971 1.073-8.74-1.685-1.226-3.525-2.146-10.877-5.367-12.562-5.981-2.91-.919-4.137.921-4.746 5.979-5.819 7.206-2.144 1.381-3.984.462-7.76-2.861-14.784-9.124c-5.465-4.873-9.154-10.891-10.228-12.73s-.114-2.835.808-3.751c.825-.824 1.838-2.147 2.759-3.22s1.224-1.84 1.836-3.065.307-2.301-.153-3.22-4.032-10.011-5.666-13.647"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
86
src/components/magicui/blur-fade-text.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, Variants } from "motion/react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface BlurFadeTextProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { y: number };
|
||||
visible: { y: number };
|
||||
};
|
||||
duration?: number;
|
||||
characterDelay?: number;
|
||||
delay?: number;
|
||||
yOffset?: number;
|
||||
animateByCharacter?: boolean;
|
||||
}
|
||||
const BlurFadeText = ({
|
||||
text,
|
||||
className,
|
||||
variant,
|
||||
duration = 0.4,
|
||||
characterDelay = 0.03,
|
||||
delay = 0,
|
||||
yOffset = 8,
|
||||
animateByCharacter = false,
|
||||
}: BlurFadeTextProps) => {
|
||||
const defaultVariants: Variants = {
|
||||
hidden: { y: -yOffset, opacity: 0, filter: "blur(8px)" },
|
||||
visible: { y: 0, opacity: 1, filter: "blur(0px)" },
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
const characters = useMemo(() => Array.from(text), [text]);
|
||||
|
||||
if (animateByCharacter) {
|
||||
return (
|
||||
<div className="flex">
|
||||
{characters.map((char, i) => {
|
||||
const charVariants: Variants = {
|
||||
hidden: { y: -yOffset, opacity: 0, filter: "blur(8px)" },
|
||||
visible: { y: 0, opacity: 1, filter: "blur(0px)" },
|
||||
};
|
||||
return (
|
||||
<motion.span
|
||||
key={i}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={charVariants}
|
||||
transition={{
|
||||
duration,
|
||||
delay: delay + i * characterDelay,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={cn("inline-block", className)}
|
||||
style={{ width: char.trim() === "" ? "0.2em" : "auto" }}
|
||||
>
|
||||
{char}
|
||||
</motion.span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<motion.span
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={combinedVariants}
|
||||
transition={{
|
||||
duration,
|
||||
delay,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{text}
|
||||
</motion.span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlurFadeText;
|
||||
63
src/components/magicui/blur-fade.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, useInView, Variants } from "motion/react";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface BlurFadeProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { y: number };
|
||||
visible: { y: number };
|
||||
};
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
yOffset?: number;
|
||||
inView?: boolean;
|
||||
inViewMargin?: string;
|
||||
blur?: string;
|
||||
}
|
||||
const BlurFade = ({
|
||||
children,
|
||||
className,
|
||||
variant,
|
||||
duration = 0.4,
|
||||
delay = 0,
|
||||
yOffset = 6,
|
||||
inView = false,
|
||||
inViewMargin = "-50px",
|
||||
blur = "6px",
|
||||
}: BlurFadeProps) => {
|
||||
const ref = useRef(null);
|
||||
const inViewResult = useInView(ref, {
|
||||
once: true,
|
||||
...(inViewMargin ? { margin: inViewMargin as any } : {})
|
||||
});
|
||||
const isInView = !inView || inViewResult;
|
||||
const defaultVariants: Variants = {
|
||||
hidden: { y: -yOffset, opacity: 0, filter: `blur(${blur})` },
|
||||
visible: { y: 0, opacity: 1, filter: `blur(0px)` },
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
exit="hidden"
|
||||
variants={combinedVariants}
|
||||
transition={{
|
||||
delay: 0.04 + delay,
|
||||
duration,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlurFade;
|
||||
91
src/components/magicui/dock.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, type MotionValue, useMotionValue, useSpring, useTransform } from "motion/react";
|
||||
import { createContext, useContext, useRef, type ReactNode } from "react";
|
||||
|
||||
interface DockProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
magnification?: number;
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
interface DockIconProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const DEFAULT_MAGNIFICATION = 60;
|
||||
const DEFAULT_DISTANCE = 100;
|
||||
const BASE_SIZE = 40;
|
||||
const BASE_ICON_SIZE = 20;
|
||||
const ICON_SIZE_RATIO = 0.5;
|
||||
const SPRING = { mass: 0.1, stiffness: 150, damping: 12 };
|
||||
|
||||
interface DockContextValue {
|
||||
mouseX: MotionValue<number>;
|
||||
magnification: number;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
const DockContext = createContext<DockContextValue | null>(null);
|
||||
|
||||
const Dock = ({ className, children, magnification = DEFAULT_MAGNIFICATION, distance = DEFAULT_DISTANCE }: DockProps) => {
|
||||
const mouseX = useMotionValue(Infinity);
|
||||
|
||||
return (
|
||||
<DockContext.Provider value={{ mouseX, magnification, distance }}>
|
||||
<motion.div
|
||||
onMouseMove={(e) => mouseX.set(e.pageX)}
|
||||
onMouseLeave={() => mouseX.set(Infinity)}
|
||||
className={cn("mx-auto w-max h-full flex items-end justify-center overflow-visible rounded-full border", className)}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</DockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const DockIcon = ({ className, children }: DockIconProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const context = useContext(DockContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("DockIcon must be used within a Dock component");
|
||||
}
|
||||
|
||||
const { mouseX, magnification, distance } = context;
|
||||
|
||||
const distanceCalc = useTransform(mouseX, (val: number) => {
|
||||
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
|
||||
return val - bounds.x - bounds.width / 2;
|
||||
});
|
||||
|
||||
const containerSize = useSpring(
|
||||
useTransform(distanceCalc, [-distance, 0, distance], [BASE_SIZE, magnification, BASE_SIZE]),
|
||||
SPRING
|
||||
);
|
||||
const iconSize = useSpring(
|
||||
useTransform(distanceCalc, [-distance, 0, distance], [BASE_ICON_SIZE, magnification * ICON_SIZE_RATIO, BASE_ICON_SIZE]),
|
||||
SPRING
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
style={{ width: containerSize, height: containerSize }}
|
||||
className={cn("relative flex aspect-square items-center justify-center rounded-full shrink-0", className)}
|
||||
>
|
||||
<motion.div
|
||||
style={{ width: iconSize, height: iconSize }}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Dock, DockIcon };
|
||||
export type { DockProps, DockIconProps };
|
||||
241
src/components/magicui/flickering-grid.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
"use client"
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
squareSize?: number
|
||||
gridGap?: number
|
||||
flickerChance?: number
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
maxOpacity?: number
|
||||
}
|
||||
|
||||
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
squareSize = 4,
|
||||
gridGap = 6,
|
||||
flickerChance = 0.3,
|
||||
color,
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
maxOpacity = 0.3,
|
||||
...props
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isInView, setIsInView] = useState(false)
|
||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 })
|
||||
const [resolvedColor, setResolvedColor] = useState<string>("rgb(0, 0, 0)")
|
||||
|
||||
const resolveColor = useCallback((colorValue: string | undefined): string => {
|
||||
if (typeof window === "undefined") {
|
||||
return "rgb(0, 0, 0)"
|
||||
}
|
||||
|
||||
const colorToResolve = colorValue || "var(--foreground)"
|
||||
|
||||
if (colorToResolve.startsWith("var(")) {
|
||||
const tempEl = document.createElement("div")
|
||||
tempEl.style.color = colorToResolve
|
||||
tempEl.style.position = "absolute"
|
||||
tempEl.style.visibility = "hidden"
|
||||
document.body.appendChild(tempEl)
|
||||
const computedColor = window.getComputedStyle(tempEl).color
|
||||
document.body.removeChild(tempEl)
|
||||
return computedColor || "rgb(0, 0, 0)"
|
||||
}
|
||||
|
||||
return colorToResolve
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const updateColor = () => {
|
||||
const resolved = resolveColor(color)
|
||||
setResolvedColor(resolved)
|
||||
}
|
||||
|
||||
updateColor()
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
updateColor()
|
||||
})
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [color, resolveColor])
|
||||
|
||||
const memoizedColor = useMemo(() => {
|
||||
const toRGBA = (colorValue: string) => {
|
||||
if (typeof window === "undefined") {
|
||||
return `rgba(0, 0, 0,`
|
||||
}
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = canvas.height = 1
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return "rgba(255, 0, 0,"
|
||||
ctx.fillStyle = colorValue
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
|
||||
return `rgba(${r}, ${g}, ${b},`
|
||||
}
|
||||
return toRGBA(resolvedColor)
|
||||
}, [resolvedColor])
|
||||
|
||||
const setupCanvas = useCallback(
|
||||
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
const cols = Math.floor(width / (squareSize + gridGap))
|
||||
const rows = Math.floor(height / (squareSize + gridGap))
|
||||
|
||||
const squares = new Float32Array(cols * rows)
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
squares[i] = Math.random() * maxOpacity
|
||||
}
|
||||
|
||||
return { cols, rows, squares, dpr }
|
||||
},
|
||||
[squareSize, gridGap, maxOpacity]
|
||||
)
|
||||
|
||||
const updateSquares = useCallback(
|
||||
(squares: Float32Array, deltaTime: number) => {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < flickerChance * deltaTime) {
|
||||
squares[i] = Math.random() * maxOpacity
|
||||
}
|
||||
}
|
||||
},
|
||||
[flickerChance, maxOpacity]
|
||||
)
|
||||
|
||||
const drawGrid = useCallback(
|
||||
(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cols: number,
|
||||
rows: number,
|
||||
squares: Float32Array,
|
||||
dpr: number
|
||||
) => {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.fillStyle = "transparent"
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j]
|
||||
ctx.fillStyle = `${memoizedColor}${opacity})`
|
||||
ctx.fillRect(
|
||||
i * (squareSize + gridGap) * dpr,
|
||||
j * (squareSize + gridGap) * dpr,
|
||||
squareSize * dpr,
|
||||
squareSize * dpr
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
[memoizedColor, squareSize, gridGap]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
const container = containerRef.current
|
||||
if (!canvas || !container) return
|
||||
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
|
||||
let animationFrameId: number
|
||||
let gridParams: ReturnType<typeof setupCanvas>
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
const newWidth = width || container.clientWidth
|
||||
const newHeight = height || container.clientHeight
|
||||
setCanvasSize({ width: newWidth, height: newHeight })
|
||||
gridParams = setupCanvas(canvas, newWidth, newHeight)
|
||||
}
|
||||
|
||||
updateCanvasSize()
|
||||
|
||||
let lastTime = 0
|
||||
const animate = (time: number) => {
|
||||
if (!isInView) return
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000
|
||||
lastTime = time
|
||||
|
||||
updateSquares(gridParams.squares, deltaTime)
|
||||
drawGrid(
|
||||
ctx,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
gridParams.cols,
|
||||
gridParams.rows,
|
||||
gridParams.squares,
|
||||
gridParams.dpr
|
||||
)
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize()
|
||||
})
|
||||
|
||||
resizeObserver.observe(container)
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsInView(entry.isIntersecting)
|
||||
},
|
||||
{ threshold: 0 }
|
||||
)
|
||||
|
||||
intersectionObserver.observe(canvas)
|
||||
|
||||
if (isInView) {
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
resizeObserver.disconnect()
|
||||
intersectionObserver.disconnect()
|
||||
}
|
||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(`h-full w-full ${className}`)}
|
||||
{...props}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="pointer-events-none"
|
||||
style={{
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
110
src/components/mdx/code-block.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, type ComponentProps } from "react";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { codeToHtml } from "shiki/bundle/web";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type CodeBlockProps = ComponentProps<"pre">;
|
||||
|
||||
function extractLanguage(className?: string): string {
|
||||
if (!className) return "plaintext";
|
||||
const match = className.match(/language-([a-z0-9-]+)/i);
|
||||
return match ? match[1] : "plaintext";
|
||||
}
|
||||
|
||||
export function CodeBlock({ children, ...props }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [{ html, className, title }, setRenderState] = useState<{
|
||||
html: string;
|
||||
className: string;
|
||||
title: string | null;
|
||||
}>({ html: "", className: "", title: null });
|
||||
const preRef = useRef<HTMLPreElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const pre = preRef.current;
|
||||
const codeEl = pre?.querySelector("code");
|
||||
if (!pre || !codeEl) return;
|
||||
|
||||
const codeText = codeEl.textContent || "";
|
||||
const lang = extractLanguage(codeEl.className);
|
||||
const nextTitle = codeEl.getAttribute("data-title");
|
||||
const nextClassName = codeEl.className || "";
|
||||
|
||||
void codeToHtml(codeText, {
|
||||
lang: lang as any,
|
||||
themes: {
|
||||
light: "github-light",
|
||||
dark: "github-dark",
|
||||
},
|
||||
defaultColor: false,
|
||||
})
|
||||
.then((html) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
setRenderState({
|
||||
html: doc.querySelector("code")?.innerHTML ?? "",
|
||||
className: nextClassName,
|
||||
title: nextTitle,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to highlight code:", error);
|
||||
setRenderState({ html: "", className: nextClassName, title: nextTitle });
|
||||
});
|
||||
}, [children]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
const code = preRef.current?.textContent || "";
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy code:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative rounded-xl overflow-hidden border border-border">
|
||||
<pre
|
||||
ref={preRef}
|
||||
{...props}
|
||||
className={cn("p-0! m-0! overflow-x-auto", props.className)}
|
||||
>
|
||||
{title && (
|
||||
<div className="p-3 text-xs font-medium border-b border-border rounded-t-xl bg-muted/50 text-foreground">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleCopy}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn("absolute size-8 text-primary cursor-pointer right-3 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity rounded-md border border-border shadow-none", title ? "top-13" : "top-3", props.className)}
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
||||
</Button>
|
||||
{html && (
|
||||
<div className="p-3">
|
||||
<code
|
||||
className={`shiki ${className}`}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!html && (
|
||||
<div className="p-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</pre >
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
34
src/components/mdx/media-container.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
interface MediaContainerProps {
|
||||
src: string;
|
||||
alt?: string;
|
||||
type?: "image" | "video";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MediaContainer({
|
||||
src,
|
||||
alt = "",
|
||||
type = "image",
|
||||
className = "",
|
||||
}: MediaContainerProps) {
|
||||
return (
|
||||
<div className={`ring-4 ring-muted w-full h-[300px] rounded-lg overflow-hidden flex items-center justify-center ${className}`}>
|
||||
{type === "image" ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="w-full h-full object-cover object-center max-w-full max-h-full"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
src={src}
|
||||
className="w-full h-full object-cover object-center max-w-full max-h-full"
|
||||
controls
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
23
src/components/mode-toggle.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ModeToggle({ className }: { className?: string }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="icon"
|
||||
className={cn(className)}
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
>
|
||||
<SunIcon className="h-full w-full" />
|
||||
<MoonIcon className="hidden h-full w-full" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
97
src/components/navbar.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Dock, DockIcon } from "@/components/magicui/dock";
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { DATA } from "@/data/resume";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-30">
|
||||
<Dock className="z-50 pointer-events-auto relative h-14 p-2 w-fit mx-auto flex gap-2 border bg-card/90 backdrop-blur-3xl shadow-[0_0_10px_3px] shadow-primary/5">
|
||||
{DATA.navbar.map((item) => {
|
||||
const isExternal = item.href.startsWith("http");
|
||||
return (
|
||||
<Tooltip key={item.href}>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={item.href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<DockIcon className="rounded-3xl cursor-pointer size-full bg-background p-0 text-muted-foreground hover:text-foreground hover:bg-muted backdrop-blur-3xl border border-border transition-colors">
|
||||
<item.icon className="size-full rounded-sm overflow-hidden object-contain" />
|
||||
</DockIcon>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
className="rounded-xl bg-primary text-primary-foreground px-4 py-2 text-sm shadow-[0_10px_40px_-10px_rgba(0,0,0,0.3)] dark:shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)]"
|
||||
>
|
||||
<p>{item.label}</p>
|
||||
<TooltipArrow className="fill-primary" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="h-2/3 m-auto w-px bg-border"
|
||||
/>
|
||||
{Object.entries(DATA.contact.social)
|
||||
.filter(([_, social]) => social.navbar)
|
||||
.map(([name, social], index) => {
|
||||
const isExternal = social.url.startsWith("http");
|
||||
const IconComponent = social.icon;
|
||||
return (
|
||||
<Tooltip key={`social-${name}-${index}`}>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={social.url}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<DockIcon className="rounded-3xl cursor-pointer size-full bg-background p-0 text-muted-foreground hover:text-foreground hover:bg-muted backdrop-blur-3xl border border-border transition-colors">
|
||||
<IconComponent className="size-full rounded-sm overflow-hidden object-contain" />
|
||||
</DockIcon>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
className="rounded-xl bg-primary text-primary-foreground px-4 py-2 text-sm shadow-[0_10px_40px_-10px_rgba(0,0,0,0.3)] dark:shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)]"
|
||||
>
|
||||
<p>{name}</p>
|
||||
<TooltipArrow className="fill-primary" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="h-2/3 m-auto w-px bg-border"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DockIcon className="rounded-3xl cursor-pointer size-full bg-background p-0 text-muted-foreground hover:text-foreground hover:bg-muted backdrop-blur-3xl border border-border transition-colors">
|
||||
<ModeToggle className="size-full cursor-pointer" />
|
||||
</DockIcon>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
className="rounded-xl bg-primary text-primary-foreground px-4 py-2 text-sm shadow-[0_10px_40px_-10px_rgba(0,0,0,0.3)] dark:shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)]"
|
||||
>
|
||||
<p>Theme</p>
|
||||
<TooltipArrow className="fill-primary" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Dock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
143
src/components/project-card.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
function ProjectImage({ src, alt }: { src: string; alt: string }) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
if (!src || imageError) {
|
||||
return <div className="w-full h-48 bg-muted" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="w-full h-48 object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href?: string;
|
||||
description: string;
|
||||
dates: string;
|
||||
tags: readonly string[];
|
||||
link?: string;
|
||||
image?: string;
|
||||
video?: string;
|
||||
links?: readonly {
|
||||
icon: React.ReactNode;
|
||||
type: string;
|
||||
href: string;
|
||||
}[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ProjectCard({
|
||||
title,
|
||||
href,
|
||||
description,
|
||||
dates,
|
||||
tags,
|
||||
link,
|
||||
image,
|
||||
video,
|
||||
links,
|
||||
className,
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col h-full border border-border rounded-xl overflow-hidden hover:ring-2 cursor-pointer hover:ring-muted transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<Link
|
||||
href={href || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
{video ? (
|
||||
<video
|
||||
src={video}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
) : image ? (
|
||||
<ProjectImage src={image} alt={title} />
|
||||
) : (
|
||||
<div className="w-full h-48 bg-muted" />
|
||||
)}
|
||||
</Link>
|
||||
{links && links.length > 0 && (
|
||||
<div className="absolute top-2 right-2 flex flex-wrap gap-2">
|
||||
{links.map((link, idx) => (
|
||||
<Link
|
||||
href={link.href}
|
||||
key={idx}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Badge
|
||||
className="flex items-center gap-1.5 text-xs bg-black text-white hover:bg-black/90"
|
||||
variant="default"
|
||||
>
|
||||
{link.icon}
|
||||
{link.type}
|
||||
</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-6 flex flex-col gap-3 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold">{title}</h3>
|
||||
<time className="text-xs text-muted-foreground">{dates}</time>
|
||||
</div>
|
||||
<Link
|
||||
href={href || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm"
|
||||
aria-label={`Open ${title}`}
|
||||
>
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-xs flex-1 prose max-w-full text-pretty font-sans leading-relaxed text-muted-foreground dark:prose-invert">
|
||||
<Markdown>{description}</Markdown>
|
||||
</div>
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-auto">
|
||||
{tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
className="text-[11px] font-medium border border-border h-6 w-fit px-2"
|
||||
variant="outline"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
src/components/section/contact-section.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
import { FlickeringGrid } from "@/components/magicui/flickering-grid";
|
||||
import { DATA } from "@/data/resume";
|
||||
|
||||
export default function ContactSection() {
|
||||
return (
|
||||
<div className="border rounded-xl p-10 relative">
|
||||
<div className="absolute -top-4 border bg-primary z-10 rounded-xl px-4 py-1 left-1/2 -translate-x-1/2">
|
||||
<span className="text-background text-sm font-medium">Contact</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 top-0 left-0 right-0 h-1/2 rounded-xl overflow-hidden">
|
||||
<FlickeringGrid
|
||||
className="h-full w-full"
|
||||
squareSize={2}
|
||||
gridGap={2}
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black, transparent)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex flex-col items-center gap-4 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">
|
||||
Get in Touch
|
||||
</h2>
|
||||
<p className="mx-auto max-w-lg text-muted-foreground text-balance">
|
||||
Want to chat? Just shoot me a dm{" "}
|
||||
<Link
|
||||
href={DATA.contact.social.X.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline underline-offset-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm"
|
||||
>
|
||||
with a direct question on twitter
|
||||
</Link>{" "}
|
||||
and I'll respond whenever I can. I will ignore all
|
||||
soliciting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
81
src/components/section/hackathons-section.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import Link from "next/link";
|
||||
import { DATA } from "@/data/resume";
|
||||
import { Timeline, TimelineItem, TimelineConnectItem } from "@/components/timeline";
|
||||
|
||||
export default function HackathonsSection() {
|
||||
return (
|
||||
<section id="hackathons" className="overflow-hidden">
|
||||
<div className="flex min-h-0 flex-col gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 items-center justify-center">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex-1 h-px bg-linear-to-r from-transparent from-5% via-border via-95% to-transparent" />
|
||||
<div className="border bg-primary z-10 rounded-xl px-4 py-1">
|
||||
<span className="text-background text-sm font-medium">Hackathons</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-linear-to-l from-transparent from-5% via-border via-95% to-transparent" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-3 items-center justify-center">
|
||||
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl">I like building things</h2>
|
||||
<p className="text-muted-foreground md:text-lg/relaxed lg:text-base/relaxed xl:text-lg/relaxed text-balance text-center">
|
||||
During my time in university, I attended {DATA.hackathons.length}+
|
||||
hackathons. People from around the country would come together and
|
||||
build incredible things in 2-3 days. It was eye-opening to see the endless possibilities brought to life by a group of motivated and passionate individuals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Timeline>
|
||||
{DATA.hackathons.map((hackathon) => (
|
||||
<TimelineItem key={hackathon.title + hackathon.dates} className="w-full flex items-start justify-between gap-10">
|
||||
<TimelineConnectItem className="flex items-start justify-center">
|
||||
{hackathon.image ? (
|
||||
<img
|
||||
src={hackathon.image}
|
||||
alt={hackathon.title}
|
||||
className="size-10 bg-card z-10 shrink-0 overflow-hidden p-1 border rounded-full shadow ring-2 ring-border object-contain flex-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-10 bg-card z-10 shrink-0 overflow-hidden p-1 border rounded-full shadow ring-2 ring-border flex-none" />
|
||||
)}
|
||||
</TimelineConnectItem>
|
||||
<div className="flex flex-1 flex-col justify-start gap-2 min-w-0">
|
||||
{hackathon.dates && (
|
||||
<time className="text-xs text-muted-foreground">{hackathon.dates}</time>
|
||||
)}
|
||||
{hackathon.title && (
|
||||
<h3 className="font-semibold leading-none">{hackathon.title}</h3>
|
||||
)}
|
||||
{hackathon.location && (
|
||||
<p className="text-sm text-muted-foreground">{hackathon.location}</p>
|
||||
)}
|
||||
{hackathon.description && (
|
||||
<p className="text-sm text-muted-foreground leading-relaxed wrap-break-word">
|
||||
{hackathon.description}
|
||||
</p>
|
||||
)}
|
||||
{hackathon.links && hackathon.links.length > 0 && (
|
||||
<div className="mt-1 flex flex-row flex-wrap items-start gap-2">
|
||||
{hackathon.links.map((link, idx) => (
|
||||
<Link
|
||||
href={link.href}
|
||||
key={idx}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge className="flex items-center gap-1.5 text-xs bg-primary text-primary-foreground">
|
||||
{link.icon}
|
||||
{link.title}
|
||||
</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TimelineItem>
|
||||
))}
|
||||
</Timeline>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
59
src/components/section/projects-section.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import BlurFade from "@/components/magicui/blur-fade";
|
||||
import { ProjectCard } from "@/components/project-card";
|
||||
import { DATA } from "@/data/resume";
|
||||
|
||||
const BLUR_FADE_DELAY = 0.04;
|
||||
|
||||
export default function ProjectsSection() {
|
||||
return (
|
||||
<section id="projects">
|
||||
<div className="flex min-h-0 flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-4 items-center justify-center">
|
||||
<div className="flex items-center w-full">
|
||||
<div
|
||||
className="flex-1 h-px bg-linear-to-r from-transparent from-5% via-border via-95% to-transparent"
|
||||
|
||||
/>
|
||||
<div className="border bg-primary z-10 rounded-xl px-4 py-1">
|
||||
<span className="text-background text-sm font-medium">My Projects</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 h-px bg-linear-to-l from-transparent from-5% via-border via-95% to-transparent"
|
||||
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-3 items-center justify-center">
|
||||
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl">Check out my latest work</h2>
|
||||
<p className="text-muted-foreground md:text-lg/relaxed lg:text-base/relaxed xl:text-lg/relaxed text-balance text-center">
|
||||
I've worked on a variety of projects, from simple
|
||||
websites to complex web applications. Here are a few of my
|
||||
favorites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 max-w-[800px] mx-auto auto-rows-fr">
|
||||
{DATA.projects.map((project, id) => (
|
||||
<BlurFade
|
||||
key={project.title}
|
||||
delay={BLUR_FADE_DELAY * 12 + id * 0.05}
|
||||
className="h-full"
|
||||
>
|
||||
<ProjectCard
|
||||
href={project.href}
|
||||
key={project.title}
|
||||
title={project.title}
|
||||
description={project.description}
|
||||
dates={project.dates}
|
||||
tags={project.technologies}
|
||||
image={project.image}
|
||||
video={project.video}
|
||||
links={project.links}
|
||||
/>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
87
src/components/section/work-section.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { DATA } from "@/data/resume";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function LogoImage({ src, alt }: { src: string; alt: string }) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
if (!src || imageError) {
|
||||
return (
|
||||
<div className="size-8 md:size-10 p-1 border rounded-full shadow ring-2 ring-border bg-muted flex-none" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="size-8 md:size-10 p-1 border rounded-full shadow ring-2 ring-border overflow-hidden object-contain flex-none"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkSection() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-full grid gap-6">
|
||||
{DATA.work.map((work) => (
|
||||
<AccordionItem
|
||||
key={work.company}
|
||||
value={work.company}
|
||||
className="w-full border-b-0 grid gap-2"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline p-0 cursor-pointer transition-colors rounded-none group [&>svg]:hidden">
|
||||
<div className="flex items-center gap-x-3 justify-between w-full text-left">
|
||||
<div className="flex items-center gap-x-3 flex-1 min-w-0">
|
||||
<LogoImage src={work.logoUrl} alt={work.company} />
|
||||
<div className="flex-1 min-w-0 gap-0.5 flex flex-col">
|
||||
<div className="font-semibold leading-none flex items-center gap-2">
|
||||
{work.company}
|
||||
<span className="relative inline-flex items-center w-3.5 h-3.5">
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"absolute h-3.5 w-3.5 shrink-0 text-muted-foreground stroke-2 transition-all duration-300 ease-out",
|
||||
"translate-x-0 opacity-0",
|
||||
"group-hover:translate-x-1 group-hover:opacity-100",
|
||||
"group-data-[state=open]:opacity-0 group-data-[state=open]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"absolute h-3.5 w-3.5 shrink-0 text-muted-foreground stroke-2 transition-all duration-200",
|
||||
"opacity-0 rotate-0",
|
||||
"group-data-[state=open]:opacity-100 group-data-[state=open]:rotate-180"
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-sans text-sm text-muted-foreground">
|
||||
{work.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs tabular-nums text-muted-foreground text-right flex-none">
|
||||
<span>
|
||||
{work.start} - {work.end ?? "Present"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="p-0 ml-13 text-xs sm:text-sm text-muted-foreground">
|
||||
{work.description}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
8
src/components/theme-provider.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
84
src/components/timeline.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Orientation = "vertical" | "horizontal";
|
||||
|
||||
export interface TimelineItemProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TimelineProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
orientation?: Orientation;
|
||||
}
|
||||
|
||||
export interface TimelineConnectItemProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimelineConnectItem({
|
||||
children,
|
||||
className,
|
||||
}: TimelineConnectItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex shrink-0 justify-center items-center self-stretch",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-timeline-line
|
||||
className={cn(
|
||||
"absolute bg-border",
|
||||
"group-data-[orientation=vertical]:left-1/2 group-data-[orientation=vertical]:-translate-x-1/2",
|
||||
"group-data-[orientation=vertical]:top-0 group-data-[orientation=vertical]:h-[calc(50%+var(--timeline-gap)+50%)]",
|
||||
"group-data-[orientation=vertical]:w-px",
|
||||
"group-data-[orientation=horizontal]:top-1/2 group-data-[orientation=horizontal]:-translate-y-1/2",
|
||||
"group-data-[orientation=horizontal]:left-1/2 group-data-[orientation=horizontal]:w-[calc(50%+var(--timeline-gap)+50%)]",
|
||||
"group-data-[orientation=horizontal]:h-px"
|
||||
)}
|
||||
/>
|
||||
<div className="relative z-20 shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimelineItem({ children, className }: TimelineItemProps) {
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Timeline({
|
||||
children,
|
||||
className,
|
||||
orientation = "vertical",
|
||||
}: TimelineProps) {
|
||||
return (
|
||||
<div
|
||||
data-orientation={orientation}
|
||||
className={cn(
|
||||
"group relative [--timeline-gap:2rem]",
|
||||
orientation === "vertical" && "flex flex-col gap-4 p-4 w-full",
|
||||
orientation === "horizontal" && "flex flex-row gap-4 p-4 h-full",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative [&>*:last-child_[data-timeline-line]]:hidden",
|
||||
orientation === "vertical" && "space-y-8 w-full",
|
||||
orientation === "horizontal" && "flex flex-row gap-8 h-full"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]_svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
||||
50
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
36
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
57
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9 rounded-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
86
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-lg bg-card text-card-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col", className)} {...props} />
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-pretty font-sans text-sm text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center pt-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
31
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
28
src/components/ui/svgs/csharp.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Csharp = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
preserveAspectRatio="xMidYMid"
|
||||
viewBox="0 -1.43 255.58 290.11"
|
||||
>
|
||||
<path
|
||||
fill="#a179dc"
|
||||
d="M255.57 84.45c0-4.83-1.04-9.1-3.13-12.76a24.4 24.4 0 0 0-9.24-9C209.17 43.05 175.1 23.5 141.1 3.86c-9.17-5.3-18.06-5.1-27.16.27-13.54 7.98-81.35 46.83-101.55 58.53C4.06 67.5.02 74.87 0 84.44v118.37c0 4.72 1 8.9 2.99 12.51 2.05 3.72 5.17 6.82 9.38 9.26 20.21 11.7 88.02 50.55 101.56 58.53 9.11 5.38 18 5.57 27.17.27 34.02-19.64 68.08-39.2 102.1-58.81a24.33 24.33 0 0 0 9.4-9.25c1.99-3.61 2.98-7.8 2.98-12.52l-.01-118.35"
|
||||
/>
|
||||
<path
|
||||
fill="#280068"
|
||||
d="M128.18 143.24 2.98 215.33c2.06 3.7 5.18 6.8 9.4 9.25 20.2 11.7 88.01 50.55 101.55 58.53 9.11 5.38 18 5.57 27.17.27 34.02-19.64 68.08-39.2 102.1-58.81a24.33 24.33 0 0 0 9.4-9.25z"
|
||||
/>
|
||||
<path
|
||||
fill="#390091"
|
||||
d="M255.57 84.45c0-4.83-1.04-9.1-3.13-12.76l-124.26 71.55 124.41 72.07c2-3.6 2.99-7.79 3-12.51 0 0 0-78.9-.02-118.35"
|
||||
/>
|
||||
<g fill="#fff">
|
||||
<path d="M201.9 116.3v13.47h13.47v-13.48h6.73v13.48h13.48v6.73H222.1v13.48h13.48v6.74H222.1v13.47h-6.73V156.7h-13.48v13.48h-6.73V156.7h-13.48v-6.73h13.47V136.5h-13.47v-6.74h13.47v-13.48zm13.47 20.2h-13.48v13.48h13.48z" />
|
||||
<path d="M128.46 48.63a94.96 94.96 0 0 1 82.26 47.45l-.16-.27-41.35 23.8A47.28 47.28 0 0 0 129 96.33h-.54a47.3 47.3 0 0 0-47.3 47.3 47.08 47.08 0 0 0 6.23 23.47 47.28 47.28 0 0 0 82.29-.27l-.2.35 41.29 23.91a94.97 94.97 0 0 1-81.25 47.54h-1.06a94.96 94.96 0 0 1-95-95 95 95 0 0 1 95-95z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Csharp };
|
||||
9
src/components/ui/svgs/docker.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Docker = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="#008fe2">
|
||||
<path d="M13.98 11.08h2.12a.19.19 0 0 0 .19-.19V9.01a.19.19 0 0 0-.19-.19h-2.12a.18.18 0 0 0-.18.18v1.9c0 .1.08.18.18.18m-2.95-5.43h2.12a.19.19 0 0 0 .18-.19V3.57a.19.19 0 0 0-.18-.18h-2.12a.18.18 0 0 0-.19.18v1.9c0 .1.09.18.19.18m0 2.71h2.12a.19.19 0 0 0 .18-.18V6.29a.19.19 0 0 0-.18-.18h-2.12a.18.18 0 0 0-.19.18v1.89c0 .1.09.18.19.18m-2.93 0h2.12a.19.19 0 0 0 .18-.18V6.29a.18.18 0 0 0-.18-.18H8.1a.18.18 0 0 0-.18.18v1.89c0 .1.08.18.18.18m-2.96 0h2.11a.19.19 0 0 0 .19-.18V6.29a.18.18 0 0 0-.19-.18H5.14a.19.19 0 0 0-.19.18v1.89c0 .1.08.18.19.18m5.89 2.72h2.12a.19.19 0 0 0 .18-.19V9.01a.19.19 0 0 0-.18-.19h-2.12a.18.18 0 0 0-.19.18v1.9c0 .1.09.18.19.18m-2.93 0h2.12a.18.18 0 0 0 .18-.19V9.01a.18.18 0 0 0-.18-.19H8.1a.18.18 0 0 0-.18.18v1.9c0 .1.08.18.18.18m-2.96 0h2.11a.18.18 0 0 0 .19-.19V9.01a.18.18 0 0 0-.18-.19H5.14a.19.19 0 0 0-.19.19v1.88c0 .1.08.19.19.19m-2.92 0h2.12a.18.18 0 0 0 .18-.19V9.01a.18.18 0 0 0-.18-.19H2.22a.18.18 0 0 0-.19.18v1.9c0 .1.08.18.19.18m21.54-1.19c-.06-.05-.67-.51-1.95-.51-.34 0-.68.03-1.01.09a3.77 3.77 0 0 0-1.72-2.57l-.34-.2-.23.33a4.6 4.6 0 0 0-.6 1.43c-.24.97-.1 1.88.4 2.66a4.7 4.7 0 0 1-1.75.42H.76a.75.75 0 0 0-.76.75 11.38 11.38 0 0 0 .7 4.06 6.03 6.03 0 0 0 2.4 3.12c1.18.73 3.1 1.14 5.28 1.14.98 0 1.96-.08 2.93-.26a12.25 12.25 0 0 0 3.82-1.4 10.5 10.5 0 0 0 2.61-2.13c1.25-1.42 2-3 2.55-4.4h.23c1.37 0 2.21-.55 2.68-1 .3-.3.55-.66.7-1.06l.1-.28Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Docker };
|
||||
20
src/components/ui/svgs/golang.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Golang = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 207 78">
|
||||
<g fill="currentColor" fillRule="evenodd">
|
||||
<path d="m16.2 24.1c-.4 0-.5-.2-.3-.5l2.1-2.7c.2-.3.7-.5 1.1-.5h35.7c.4 0 .5.3.3.6l-1.7 2.6c-.2.3-.7.6-1 .6z" />
|
||||
<path d="m1.1 33.3c-.4 0-.5-.2-.3-.5l2.1-2.7c.2-.3.7-.5 1.1-.5h45.6c.4 0 .6.3.5.6l-.8 2.4c-.1.4-.5.6-.9.6z" />
|
||||
<path d="m25.3 42.5c-.4 0-.5-.3-.3-.6l1.4-2.5c.2-.3.6-.6 1-.6h20c.4 0 .6.3.6.7l-.2 2.4c0 .4-.4.7-.7.7z" />
|
||||
<g transform="translate(55)">
|
||||
<path d="m74.1 22.3c-6.3 1.6-10.6 2.8-16.8 4.4-1.5.4-1.6.5-2.9-1-1.5-1.7-2.6-2.8-4.7-3.8-6.3-3.1-12.4-2.2-18.1 1.5-6.8 4.4-10.3 10.9-10.2 19 .1 8 5.6 14.6 13.5 15.7 6.8.9 12.5-1.5 17-6.6.9-1.1 1.7-2.3 2.7-3.7-3.6 0-8.1 0-19.3 0-2.1 0-2.6-1.3-1.9-3 1.3-3.1 3.7-8.3 5.1-10.9.3-.6 1-1.6 2.5-1.6h36.4c-.2 2.7-.2 5.4-.6 8.1-1.1 7.2-3.8 13.8-8.2 19.6-7.2 9.5-16.6 15.4-28.5 17-9.8 1.3-18.9-.6-26.9-6.6-7.4-5.6-11.6-13-12.7-22.2-1.3-10.9 1.9-20.7 8.5-29.3 7.1-9.3 16.5-15.2 28-17.3 9.4-1.7 18.4-.6 26.5 4.9 5.3 3.5 9.1 8.3 11.6 14.1.6.9.2 1.4-1 1.7z" />
|
||||
<path
|
||||
d="m107.2 77.6c-9.1-.2-17.4-2.8-24.4-8.8-5.9-5.1-9.6-11.6-10.8-19.3-1.8-11.3 1.3-21.3 8.1-30.2 7.3-9.6 16.1-14.6 28-16.7 10.2-1.8 19.8-.8 28.5 5.1 7.9 5.4 12.8 12.7 14.1 22.3 1.7 13.5-2.2 24.5-11.5 33.9-6.6 6.7-14.7 10.9-24 12.8-2.7.5-5.4.6-8 .9zm23.8-40.4c-.1-1.3-.1-2.3-.3-3.3-1.8-9.9-10.9-15.5-20.4-13.3-9.3 2.1-15.3 8-17.5 17.4-1.8 7.8 2 15.7 9.2 18.9 5.5 2.4 11 2.1 16.3-.6 7.9-4.1 12.2-10.5 12.7-19.1z"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Golang };
|
||||
20
src/components/ui/svgs/golangDark.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const GolangDark = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 207 78">
|
||||
<g fill="#fff" fillRule="evenodd">
|
||||
<path d="m16.2 24.1c-.4 0-.5-.2-.3-.5l2.1-2.7c.2-.3.7-.5 1.1-.5h35.7c.4 0 .5.3.3.6l-1.7 2.6c-.2.3-.7.6-1 .6z" />
|
||||
<path d="m1.1 33.3c-.4 0-.5-.2-.3-.5l2.1-2.7c.2-.3.7-.5 1.1-.5h45.6c.4 0 .6.3.5.6l-.8 2.4c-.1.4-.5.6-.9.6z" />
|
||||
<path d="m25.3 42.5c-.4 0-.5-.3-.3-.6l1.4-2.5c.2-.3.6-.6 1-.6h20c.4 0 .6.3.6.7l-.2 2.4c0 .4-.4.7-.7.7z" />
|
||||
<g transform="translate(55)">
|
||||
<path d="m74.1 22.3c-6.3 1.6-10.6 2.8-16.8 4.4-1.5.4-1.6.5-2.9-1-1.5-1.7-2.6-2.8-4.7-3.8-6.3-3.1-12.4-2.2-18.1 1.5-6.8 4.4-10.3 10.9-10.2 19 .1 8 5.6 14.6 13.5 15.7 6.8.9 12.5-1.5 17-6.6.9-1.1 1.7-2.3 2.7-3.7-3.6 0-8.1 0-19.3 0-2.1 0-2.6-1.3-1.9-3 1.3-3.1 3.7-8.3 5.1-10.9.3-.6 1-1.6 2.5-1.6h36.4c-.2 2.7-.2 5.4-.6 8.1-1.1 7.2-3.8 13.8-8.2 19.6-7.2 9.5-16.6 15.4-28.5 17-9.8 1.3-18.9-.6-26.9-6.6-7.4-5.6-11.6-13-12.7-22.2-1.3-10.9 1.9-20.7 8.5-29.3 7.1-9.3 16.5-15.2 28-17.3 9.4-1.7 18.4-.6 26.5 4.9 5.3 3.5 9.1 8.3 11.6 14.1.6.9.2 1.4-1 1.7z" />
|
||||
<path
|
||||
d="m107.2 77.6c-9.1-.2-17.4-2.8-24.4-8.8-5.9-5.1-9.6-11.6-10.8-19.3-1.8-11.3 1.3-21.3 8.1-30.2 7.3-9.6 16.1-14.6 28-16.7 10.2-1.8 19.8-.8 28.5 5.1 7.9 5.4 12.8 12.7 14.1 22.3 1.7 13.5-2.2 24.5-11.5 33.9-6.6 6.7-14.7 10.9-24 12.8-2.7.5-5.4.6-8 .9zm23.8-40.4c-.1-1.3-.1-2.3-.3-3.3-1.8-9.9-10.9-15.5-20.4-13.3-9.3 2.1-15.3 8-17.5 17.4-1.8 7.8 2 15.7 9.2 18.9 5.5 2.4 11 2.1 16.3-.6 7.9-4.1 12.2-10.5 12.7-19.1z"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { GolangDark };
|
||||
28
src/components/ui/svgs/java.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Java = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} preserveAspectRatio="xMidYMid" viewBox="0 0 256 346">
|
||||
<path
|
||||
d="M83 267s-14 8 9 11c27 3 41 2 71-3 0 0 8 5 19 9-67 29-153-2-99-17M74 230s-15 11 8 13c29 3 52 3 92-4 0 0 6 5 15 8-82 24-173 2-115-17"
|
||||
fill="#5382A1"
|
||||
/>
|
||||
<path
|
||||
d="M144 166c17 19-4 36-4 36s42-22 22-49c-18-26-32-38 44-82 0 0-119 29-62 95"
|
||||
fill="#E76F00"
|
||||
/>
|
||||
<path
|
||||
d="M233 295s10 8-10 15c-39 12-163 15-197 0-12-5 11-13 18-14l12-2c-14-9-89 19-38 28 138 22 251-10 215-27M89 190s-63 15-22 21c17 2 51 2 83-1 26-2 52-7 52-7l-16 9c-64 16-187 8-151-9 30-14 54-13 54-13M202 253c64-33 34-66 13-61l-7 2s2-3 6-5c41-14 73 43-14 66l2-2"
|
||||
fill="#5382A1"
|
||||
/>
|
||||
<path
|
||||
d="M162 0s36 36-34 91c-56 45-12 70 0 99-32-30-56-56-40-80 23-35 89-53 74-110"
|
||||
fill="#E76F00"
|
||||
/>
|
||||
<path
|
||||
d="M95 345c62 4 158-3 160-32 0 0-4 11-51 20-53 10-119 9-158 2 0 0 8 7 49 10"
|
||||
fill="#5382A1"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Java };
|
||||
58
src/components/ui/svgs/kubernetes.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Kubernetes = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 722.8 702">
|
||||
<path
|
||||
style={{
|
||||
fill: "#326ce5",
|
||||
fillOpacity: "1",
|
||||
stroke: "#fff",
|
||||
strokeWidth: "0",
|
||||
strokeMiterlimit: "4",
|
||||
strokeOpacity: "1",
|
||||
strokeDasharray: "none",
|
||||
}}
|
||||
d="M365 185a47 46 0 0 0-18 4L103 306a47 46 0 0 0-25 32L18 600a47 46 0 0 0 6 35 47 46 0 0 0 3 4l169 210a47 46 0 0 0 36 18h271a47 46 0 0 0 37-18l169-210a47 46 0 0 0 9-39l-60-263a47 46 0 0 0-26-31L388 189a47 46 0 0 0-23-4z"
|
||||
transform="translate(-6 -175)"
|
||||
/>
|
||||
<path
|
||||
d="M368 274c-8 0-15 7-15 16v4l2 14 2 27c-1 3-3 6-5 7v7a190 190 0 0 0-122 58l-6-3c-2 0-5 1-8-1l-20-18-10-10-3-3c-3-2-7-4-10-4-5 0-9 2-12 5-5 7-3 16 4 22l3 3 12 7 22 15c2 2 3 7 3 8l5 5c-26 37-37 84-30 131l-6 2c-2 2-4 6-7 7l-26 4-14 1-4 1c-9 2-14 10-13 18 2 8 11 12 19 11h1v-1h4l13-5c9-4 17-7 25-8l9 3 6-1c15 45 45 82 84 105l-2 7c1 2 2 5 1 8l-13 24-8 11-2 4c-4 8-1 18 6 21 8 4 17 0 20-8l2-4 5-13c3-10 6-20 11-27l6-3 3-6a189 189 0 0 0 135 1l4 5c2 1 5 2 7 5l10 24 4 14 2 4c4 8 13 11 20 8 8-4 10-13 7-21l-2-4-8-12c-6-8-10-15-13-23-1-4 0-6 1-8l-2-6c40-24 70-62 84-106l6 1c2-2 4-4 8-3 8 1 16 4 26 7l13 5 4 1c9 2 17-3 19-10 2-8-4-16-12-18l-5-1-14-1c-10-1-18-2-26-5-3-1-5-5-6-6l-6-2a189 189 0 0 0-31-131l6-5c0-3 0-5 2-8 6-6 13-10 22-16l12-7 4-2c7-6 8-16 3-22s-15-7-22-1l-3 2-10 11c-7 7-13 13-19 17-3 2-7 2-9 1l-6 4c-31-33-75-54-121-58v-7c-2-2-5-3-5-7-1-8 0-16 1-27l2-14v-4c0-9-6-16-14-16zm-19 113-4 77a13 13 0 0 1-21 10l-63-44a151 151 0 0 1 88-43zm37 0c33 5 64 20 88 43l-63 44a13 13 0 0 1-21-10zm-148 71 58 52a13 13 0 0 1-5 22l-74 22c-4-35 4-68 21-96zm259 0a153 153 0 0 1 22 95l-74-21a13 13 0 0 1-5-22h-1l58-52zm-141 56h23l15 18-5 23-21 10-21-10-6-23zm75 62h3l77 13c-12 32-33 59-61 77l-30-72a13 13 0 0 1 11-18zm-128 1a13 13 0 0 1 12 18l-29 71c-27-18-49-44-61-77l76-12h2zm64 31c2-1 4 0 6 1 3 1 5 3 6 5l38 68a154 154 0 0 1-98-1l37-67c3-4 7-6 11-6z"
|
||||
style={{
|
||||
fontSize: "medium",
|
||||
fontStyle: "normal",
|
||||
fontVariant: "normal",
|
||||
fontWeight: "400",
|
||||
fontStretch: "normal",
|
||||
textIndent: "0",
|
||||
textAlign: "start",
|
||||
textDecoration: "none",
|
||||
lineHeight: "normal",
|
||||
letterSpacing: "normal",
|
||||
wordSpacing: "normal",
|
||||
textTransform: "none",
|
||||
direction: "ltr",
|
||||
blockProgression: "tb",
|
||||
writingMode: "lr-tb" as any,
|
||||
textAnchor: "start",
|
||||
baselineShift: "baseline",
|
||||
color: "#000",
|
||||
fill: "#fff",
|
||||
fillOpacity: "1",
|
||||
stroke: "#fff",
|
||||
strokeWidth: ".25",
|
||||
strokeMiterlimit: "4",
|
||||
strokeOpacity: "1",
|
||||
strokeDasharray: "none",
|
||||
marker: "none",
|
||||
visibility: "visible",
|
||||
display: "inline",
|
||||
overflow: "visible",
|
||||
fontFamily: "Sans",
|
||||
InkscapeFontSpecification: "Sans",
|
||||
} as React.CSSProperties}
|
||||
transform="translate(-6 -175)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Kubernetes };
|
||||
57
src/components/ui/svgs/nextjsIconDark.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const NextjsIconDark = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 180 180">
|
||||
<mask
|
||||
height="180"
|
||||
id=":r8:mask0_408_134"
|
||||
maskUnits="userSpaceOnUse"
|
||||
width="180"
|
||||
x="0"
|
||||
y="0"
|
||||
style={{ maskType: "alpha" }}
|
||||
>
|
||||
<circle cx="90" cy="90" fill="black" r="90" />
|
||||
</mask>
|
||||
<g mask="url(#:r8:mask0_408_134)">
|
||||
<circle cx="90" cy="90" data-circle="true" fill="black" r="90" />
|
||||
<path
|
||||
d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
|
||||
fill="url(#:r8:paint0_linear_408_134)"
|
||||
/>
|
||||
<rect
|
||||
fill="url(#:r8:paint1_linear_408_134)"
|
||||
height="72"
|
||||
width="12"
|
||||
x="115"
|
||||
y="54"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id=":r8:paint0_linear_408_134"
|
||||
x1="109"
|
||||
x2="144.5"
|
||||
y1="116.5"
|
||||
y2="160.5"
|
||||
>
|
||||
<stop stopColor="white" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id=":r8:paint1_linear_408_134"
|
||||
x1="121"
|
||||
x2="120.799"
|
||||
y1="54"
|
||||
y2="106.875"
|
||||
>
|
||||
<stop stopColor="white" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { NextjsIconDark };
|
||||
42
src/components/ui/svgs/nextjsLogoDark.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const NextjsLogoDark = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 394 80" fill="none">
|
||||
<path
|
||||
d="M261.919 0.0330722H330.547V12.7H303.323V79.339H289.71V12.7H261.919V0.0330722Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M149.052 0.0330722V12.7H94.0421V33.0772H138.281V45.7441H94.0421V66.6721H149.052V79.339H80.43V12.7H80.4243V0.0330722H149.052Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M183.32 0.0661486H165.506L229.312 79.3721H247.178L215.271 39.7464L247.127 0.126654L229.312 0.154184L206.352 28.6697L183.32 0.0661486Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M201.6 56.7148L192.679 45.6229L165.455 79.4326H183.32L201.6 56.7148Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M80.907 79.339L17.0151 0H0V79.3059H13.6121V16.9516L63.8067 79.339H80.907Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M333.607 78.8546C332.61 78.8546 331.762 78.5093 331.052 77.8186C330.342 77.1279 329.991 76.2917 330 75.3011C329.991 74.3377 330.342 73.5106 331.052 72.8199C331.762 72.1292 332.61 71.7838 333.607 71.7838C334.566 71.7838 335.405 72.1292 336.115 72.8199C336.835 73.5106 337.194 74.3377 337.204 75.3011C337.194 75.9554 337.028 76.5552 336.696 77.0914C336.355 77.6368 335.922 78.064 335.377 78.373C334.842 78.6911 334.252 78.8546 333.607 78.8546Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M356.84 45.4453H362.872V68.6846C362.863 70.8204 362.401 72.6472 361.498 74.1832C360.585 75.7191 359.321 76.8914 357.698 77.7185C356.084 78.5364 354.193 78.9546 352.044 78.9546C350.079 78.9546 348.318 78.6001 346.75 77.9094C345.182 77.2187 343.937 76.1826 343.024 74.8193C342.101 73.456 341.649 71.7565 341.649 69.7207H347.691C347.7 70.6114 347.903 71.3838 348.29 72.0291C348.677 72.6744 349.212 73.1651 349.895 73.5105C350.586 73.8559 351.38 74.0286 352.274 74.0286C353.243 74.0286 354.073 73.8286 354.746 73.4196C355.419 73.0197 355.936 72.4199 356.296 71.6201C356.646 70.8295 356.831 69.8479 356.84 68.6846V45.4453Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M387.691 54.5338C387.544 53.1251 386.898 52.0254 385.773 51.2438C384.638 50.4531 383.172 50.0623 381.373 50.0623C380.11 50.0623 379.022 50.2532 378.118 50.6258C377.214 51.0075 376.513 51.5164 376.033 52.1617C375.554 52.807 375.314 53.5432 375.295 54.3703C375.295 55.061 375.461 55.6608 375.784 56.1607C376.107 56.6696 376.54 57.0968 377.103 57.4422C377.656 57.7966 378.274 58.0874 378.948 58.3237C379.63 58.56 380.313 58.76 380.995 58.9236L384.14 59.6961C385.404 59.9869 386.631 60.3778 387.802 60.8776C388.973 61.3684 390.034 61.9955 390.965 62.7498C391.897 63.5042 392.635 64.413 393.179 65.4764C393.723 66.5397 394 67.7848 394 69.2208C394 71.1566 393.502 72.8562 392.496 74.3285C391.491 75.7917 390.043 76.9369 388.143 77.764C386.252 78.582 383.965 79 381.272 79C378.671 79 376.402 78.6002 374.493 77.8004C372.575 77.0097 371.08 75.8463 370.001 74.3194C368.922 72.7926 368.341 70.9294 368.258 68.7391H374.235C374.318 69.8842 374.687 70.8386 375.314 71.6111C375.95 72.3745 376.78 72.938 377.795 73.3197C378.819 73.6923 379.962 73.8832 381.226 73.8832C382.545 73.8832 383.707 73.6832 384.712 73.2924C385.708 72.9016 386.492 72.3564 387.055 71.6475C387.627 70.9476 387.913 70.1206 387.922 69.1754C387.913 68.312 387.654 67.5939 387.156 67.0304C386.649 66.467 385.948 65.9944 385.053 65.6127C384.15 65.231 383.098 64.8856 381.899 64.5857L378.081 63.6223C375.323 62.9225 373.137 61.8592 371.541 60.4323C369.937 59.0054 369.143 57.115 369.143 54.7429C369.143 52.798 369.678 51.0894 370.758 49.6261C371.827 48.1629 373.294 47.0268 375.148 46.2179C377.011 45.4 379.114 45 381.456 45C383.836 45 385.92 45.4 387.719 46.2179C389.517 47.0268 390.929 48.1538 391.952 49.5897C392.976 51.0257 393.511 52.6707 393.539 54.5338H387.691Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { NextjsLogoDark };
|
||||
42
src/components/ui/svgs/nextjsLogoLight.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const NextjsLogoLight = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 394 80" fill="none">
|
||||
<path
|
||||
d="M261.919 0.0330722H330.547V12.7H303.323V79.339H289.71V12.7H261.919V0.0330722Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M149.052 0.0330722V12.7H94.0421V33.0772H138.281V45.7441H94.0421V66.6721H149.052V79.339H80.43V12.7H80.4243V0.0330722H149.052Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M183.32 0.0661486H165.506L229.312 79.3721H247.178L215.271 39.7464L247.127 0.126654L229.312 0.154184L206.352 28.6697L183.32 0.0661486Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M201.6 56.7148L192.679 45.6229L165.455 79.4326H183.32L201.6 56.7148Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M80.907 79.339L17.0151 0H0V79.3059H13.6121V16.9516L63.8067 79.339H80.907Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M333.607 78.8546C332.61 78.8546 331.762 78.5093 331.052 77.8186C330.342 77.1279 329.991 76.2917 330 75.3011C329.991 74.3377 330.342 73.5106 331.052 72.8199C331.762 72.1292 332.61 71.7838 333.607 71.7838C334.566 71.7838 335.405 72.1292 336.115 72.8199C336.835 73.5106 337.194 74.3377 337.204 75.3011C337.194 75.9554 337.028 76.5552 336.696 77.0914C336.355 77.6368 335.922 78.064 335.377 78.373C334.842 78.6911 334.252 78.8546 333.607 78.8546Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M356.84 45.4453H362.872V68.6846C362.863 70.8204 362.401 72.6472 361.498 74.1832C360.585 75.7191 359.321 76.8914 357.698 77.7185C356.084 78.5364 354.193 78.9546 352.044 78.9546C350.079 78.9546 348.318 78.6001 346.75 77.9094C345.182 77.2187 343.937 76.1826 343.024 74.8193C342.101 73.456 341.649 71.7565 341.649 69.7207H347.691C347.7 70.6114 347.903 71.3838 348.29 72.0291C348.677 72.6744 349.212 73.1651 349.895 73.5105C350.586 73.8559 351.38 74.0286 352.274 74.0286C353.243 74.0286 354.073 73.8286 354.746 73.4196C355.419 73.0197 355.936 72.4199 356.296 71.6201C356.646 70.8295 356.831 69.8479 356.84 68.6846V45.4453Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M387.691 54.5338C387.544 53.1251 386.898 52.0254 385.773 51.2438C384.638 50.4531 383.172 50.0623 381.373 50.0623C380.11 50.0623 379.022 50.2532 378.118 50.6258C377.214 51.0075 376.513 51.5164 376.033 52.1617C375.554 52.807 375.314 53.5432 375.295 54.3703C375.295 55.061 375.461 55.6608 375.784 56.1607C376.107 56.6696 376.54 57.0968 377.103 57.4422C377.656 57.7966 378.274 58.0874 378.948 58.3237C379.63 58.56 380.313 58.76 380.995 58.9236L384.14 59.6961C385.404 59.9869 386.631 60.3778 387.802 60.8776C388.973 61.3684 390.034 61.9955 390.965 62.7498C391.897 63.5042 392.635 64.413 393.179 65.4764C393.723 66.5397 394 67.7848 394 69.2208C394 71.1566 393.502 72.8562 392.496 74.3285C391.491 75.7917 390.043 76.9369 388.143 77.764C386.252 78.582 383.965 79 381.272 79C378.671 79 376.402 78.6002 374.493 77.8004C372.575 77.0097 371.08 75.8463 370.001 74.3194C368.922 72.7926 368.341 70.9294 368.258 68.7391H374.235C374.318 69.8842 374.687 70.8386 375.314 71.6111C375.95 72.3745 376.78 72.938 377.795 73.3197C378.819 73.6923 379.962 73.8832 381.226 73.8832C382.545 73.8832 383.707 73.6832 384.712 73.2924C385.708 72.9016 386.492 72.3564 387.055 71.6475C387.627 70.9476 387.913 70.1206 387.922 69.1754C387.913 68.312 387.654 67.5939 387.156 67.0304C386.649 66.467 385.948 65.9944 385.053 65.6127C384.15 65.231 383.098 64.8856 381.899 64.5857L378.081 63.6223C375.323 62.9225 373.137 61.8592 371.541 60.4323C369.937 59.0054 369.143 57.115 369.143 54.7429C369.143 52.798 369.678 51.0894 370.758 49.6261C371.827 48.1629 373.294 47.0268 375.148 46.2179C377.011 45.4 379.114 45 381.456 45C383.836 45 385.92 45.4 387.719 46.2179C389.517 47.0268 390.929 48.1538 391.952 49.5897C392.976 51.0257 393.511 52.6707 393.539 54.5338H387.691Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { NextjsLogoLight };
|
||||
79
src/components/ui/svgs/nodejs.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Nodejs = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 256 292"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="68.188%"
|
||||
x2="27.823%"
|
||||
y1="17.487%"
|
||||
y2="89.755%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#41873F" />
|
||||
<stop offset="32.88%" stopColor="#418B3D" />
|
||||
<stop offset="63.52%" stopColor="#419637" />
|
||||
<stop offset="93.19%" stopColor="#3FA92D" />
|
||||
<stop offset="100%" stopColor="#3FAE2A" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="c"
|
||||
x1="43.277%"
|
||||
x2="159.245%"
|
||||
y1="55.169%"
|
||||
y2="-18.306%"
|
||||
>
|
||||
<stop offset="13.76%" stopColor="#41873F" />
|
||||
<stop offset="40.32%" stopColor="#54A044" />
|
||||
<stop offset="71.36%" stopColor="#66B848" />
|
||||
<stop offset="90.81%" stopColor="#6CC04A" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="f"
|
||||
x1="-4.389%"
|
||||
x2="101.499%"
|
||||
y1="49.997%"
|
||||
y2="49.997%"
|
||||
>
|
||||
<stop offset="9.192%" stopColor="#6CC04A" />
|
||||
<stop offset="28.64%" stopColor="#66B848" />
|
||||
<stop offset="59.68%" stopColor="#54A044" />
|
||||
<stop offset="86.24%" stopColor="#41873F" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="b"
|
||||
d="M134.923 1.832c-4.344-2.443-9.502-2.443-13.846 0L6.787 67.801C2.443 70.244 0 74.859 0 79.745v132.208c0 4.887 2.715 9.502 6.787 11.945l114.29 65.968c4.344 2.444 9.502 2.444 13.846 0l114.29-65.968c4.344-2.443 6.787-7.058 6.787-11.945V79.745c0-4.886-2.715-9.501-6.787-11.944L134.923 1.832Z"
|
||||
/>
|
||||
<path
|
||||
id="e"
|
||||
d="M134.923 1.832c-4.344-2.443-9.502-2.443-13.846 0L6.787 67.801C2.443 70.244 0 74.859 0 79.745v132.208c0 4.887 2.715 9.502 6.787 11.945l114.29 65.968c4.344 2.444 9.502 2.444 13.846 0l114.29-65.968c4.344-2.443 6.787-7.058 6.787-11.945V79.745c0-4.886-2.715-9.501-6.787-11.944L134.923 1.832Z"
|
||||
/>
|
||||
</defs>
|
||||
<path
|
||||
fill="url(#a)"
|
||||
d="M134.923 1.832c-4.344-2.443-9.502-2.443-13.846 0L6.787 67.801C2.443 70.244 0 74.859 0 79.745v132.208c0 4.887 2.715 9.502 6.787 11.945l114.29 65.968c4.344 2.444 9.502 2.444 13.846 0l114.29-65.968c4.344-2.443 6.787-7.058 6.787-11.945V79.745c0-4.886-2.715-9.501-6.787-11.944L134.923 1.832Z"
|
||||
/>
|
||||
<mask id="d" fill="#fff">
|
||||
<use xlinkHref="#b" />
|
||||
</mask>
|
||||
<path
|
||||
fill="url(#c)"
|
||||
d="M249.485 67.8 134.65 1.833c-1.086-.542-2.443-1.085-3.529-1.357L2.443 220.912c1.086 1.357 2.444 2.443 3.8 3.258l114.834 65.968c3.258 1.9 7.059 2.443 10.588 1.357L252.47 70.515c-.815-1.086-1.9-1.9-2.986-2.714Z"
|
||||
mask="url(#d)"
|
||||
/>
|
||||
<mask id="g" fill="#fff">
|
||||
<use xlinkHref="#e" />
|
||||
</mask>
|
||||
<path
|
||||
fill="url(#f)"
|
||||
d="M249.756 223.898c3.258-1.9 5.701-5.158 6.787-8.687L130.579.204c-3.258-.543-6.787-.272-9.773 1.628L6.786 67.53l122.979 224.238c1.628-.272 3.529-.815 5.158-1.63l114.833-66.239Z"
|
||||
mask="url(#g)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Nodejs };
|
||||
62
src/components/ui/svgs/postgresql.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Postgresql = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlSpace="preserve" viewBox="0 0 432.071 445.383">
|
||||
<g
|
||||
style={{
|
||||
fillRule: "nonzero",
|
||||
clipRule: "nonzero",
|
||||
fill: "none",
|
||||
stroke: "#fff",
|
||||
strokeWidth: "12.4651",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeMiterlimit: "4",
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M323.205 324.227c2.833-23.601 1.984-27.062 19.563-23.239l4.463.392c13.517.615 31.199-2.174 41.587-7 22.362-10.376 35.622-27.7 13.572-23.148-50.297 10.376-53.755-6.655-53.755-6.655 53.111-78.803 75.313-178.836 56.149-203.322-52.27-66.789-142.748-35.206-144.262-34.386l-.482.089c-9.938-2.062-21.06-3.294-33.554-3.496-22.761-.374-40.032 5.967-53.133 15.904 0 0-161.408-66.498-153.899 83.628 1.597 31.936 45.777 241.655 98.47 178.31 19.259-23.163 37.871-42.748 37.871-42.748 9.242 6.14 20.307 9.272 31.912 8.147l.897-.765c-.281 2.876-.157 5.689.359 9.019-13.572 15.167-9.584 17.83-36.723 23.416-27.457 5.659-11.326 15.734-.797 18.367 12.768 3.193 42.305 7.716 62.268-20.224l-.795 3.188c5.325 4.26 4.965 30.619 5.72 49.452.756 18.834 2.017 36.409 5.856 46.771 3.839 10.36 8.369 37.05 44.036 29.406 29.809-6.388 52.6-15.582 54.677-101.107"
|
||||
style={{
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: "37.3953",
|
||||
strokeLinecap: "butt",
|
||||
strokeLinejoin: "miter",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M402.395 271.23c-50.302 10.376-53.76-6.655-53.76-6.655 53.111-78.808 75.313-178.843 56.153-203.326-52.27-66.785-142.752-35.2-144.262-34.38l-.486.087c-9.938-2.063-21.06-3.292-33.56-3.496-22.761-.373-40.026 5.967-53.127 15.902 0 0-161.411-66.495-153.904 83.63 1.597 31.938 45.776 241.657 98.471 178.312 19.26-23.163 37.869-42.748 37.869-42.748 9.243 6.14 20.308 9.272 31.908 8.147l.901-.765c-.28 2.876-.152 5.689.361 9.019-13.575 15.167-9.586 17.83-36.723 23.416-27.459 5.659-11.328 15.734-.796 18.367 12.768 3.193 42.307 7.716 62.266-20.224l-.796 3.188c5.319 4.26 9.054 27.711 8.428 48.969-.626 21.259-1.044 35.854 3.147 47.254 4.191 11.4 8.368 37.05 44.042 29.406 29.809-6.388 45.256-22.942 47.405-50.555 1.525-19.631 4.976-16.729 5.194-34.28l2.768-8.309c3.192-26.611.507-35.196 18.872-31.203l4.463.392c13.517.615 31.208-2.174 41.591-7 22.358-10.376 35.618-27.7 13.573-23.148z"
|
||||
style={{ fill: "#336791", stroke: "none" }}
|
||||
stroke="none"
|
||||
/>
|
||||
<path d="M215.866 286.484c-1.385 49.516.348 99.377 5.193 111.495 4.848 12.118 15.223 35.688 50.9 28.045 29.806-6.39 40.651-18.756 45.357-46.051 3.466-20.082 10.148-75.854 11.005-87.281M173.104 38.256S11.583-27.76 19.092 122.365c1.597 31.938 45.779 241.664 98.473 178.316 19.256-23.166 36.671-41.335 36.671-41.335M260.349 26.207c-5.591 1.753 89.848-34.889 144.087 34.417 19.159 24.484-3.043 124.519-56.153 203.329" />
|
||||
<path
|
||||
d="M348.282 263.953s3.461 17.036 53.764 6.653c22.04-4.552 8.776 12.774-13.577 23.155-18.345 8.514-59.474 10.696-60.146-1.069-1.729-30.355 21.647-21.133 19.96-28.739-1.525-6.85-11.979-13.573-18.894-30.338-6.037-14.633-82.796-126.849 21.287-110.183 3.813-.789-27.146-99.002-124.553-100.599-97.385-1.597-94.19 119.762-94.19 119.762"
|
||||
style={{ strokeLinejoin: "bevel" }}
|
||||
/>
|
||||
<path d="M188.604 274.334c-13.577 15.166-9.584 17.829-36.723 23.417-27.459 5.66-11.326 15.733-.797 18.365 12.768 3.195 42.307 7.718 62.266-20.229 6.078-8.509-.036-22.086-8.385-25.547-4.034-1.671-9.428-3.765-16.361 3.994z" />
|
||||
<path d="M187.715 274.069c-1.368-8.917 2.93-19.528 7.536-31.942 6.922-18.626 22.893-37.255 10.117-96.339-9.523-44.029-73.396-9.163-73.436-3.193-.039 5.968 2.889 30.26-1.067 58.548-5.162 36.913 23.488 68.132 56.479 64.938" />
|
||||
<path
|
||||
d="M172.517 141.7c-.288 2.039 3.733 7.48 8.976 8.207 5.234.73 9.714-3.522 9.998-5.559.284-2.039-3.732-4.285-8.977-5.015-5.237-.731-9.719.333-9.996 2.367z"
|
||||
style={{
|
||||
fill: "#fff",
|
||||
strokeWidth: "4.155",
|
||||
strokeLinecap: "butt",
|
||||
strokeLinejoin: "miter",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M331.941 137.543c.284 2.039-3.732 7.48-8.976 8.207-5.238.73-9.718-3.522-10.005-5.559-.277-2.039 3.74-4.285 8.979-5.015 5.239-.73 9.718.333 10.002 2.368z"
|
||||
style={{
|
||||
fill: "#fff",
|
||||
strokeWidth: "2.0775",
|
||||
strokeLinecap: "butt",
|
||||
strokeLinejoin: "miter",
|
||||
}}
|
||||
/>
|
||||
<path d="M350.676 123.432c.863 15.994-3.445 26.888-3.988 43.914-.804 24.748 11.799 53.074-7.191 81.435" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Postgresql };
|
||||
77
src/components/ui/svgs/postgresqlWordmarkDark.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const PostgresqlWordmarkDark = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 1416.8 445.5">
|
||||
<path
|
||||
stroke="#000"
|
||||
strokeWidth="37.395"
|
||||
d="M323.175 324.211c2.833-23.601 1.984-27.062 19.563-23.239l4.463.392c13.517.615 31.199-2.174 41.587-7 22.362-10.376 35.622-27.7 13.572-23.148-50.297 10.376-53.755-6.655-53.755-6.655 53.111-78.803 75.313-178.836 56.149-203.322-52.27-66.789-142.748-35.206-144.262-34.386l-.482.089c-9.938-2.062-21.06-3.294-33.554-3.496-22.761-.374-40.032 5.967-53.133 15.904 0 0-161.408-66.498-153.899 83.628 1.597 31.936 45.777 241.655 98.47 178.31 19.259-23.163 37.871-42.748 37.871-42.748 9.242 6.14 20.307 9.272 31.912 8.147l.897-.765c-.281 2.876-.157 5.689.359 9.019-13.572 15.167-9.584 17.83-36.723 23.416-27.457 5.659-11.326 15.734-.797 18.367 12.768 3.193 42.305 7.716 62.268-20.224l-.795 3.188c5.325 4.26 4.965 30.619 5.72 49.452.756 18.834 2.017 36.409 5.856 46.771 3.839 10.36 8.369 37.05 44.036 29.406 29.809-6.388 52.6-15.582 54.677-101.107"
|
||||
/>
|
||||
<path
|
||||
fill="#336791"
|
||||
d="M402.365 271.214c-50.302 10.376-53.76-6.655-53.76-6.655 53.111-78.808 75.313-178.843 56.153-203.326-52.27-66.785-142.752-35.2-144.262-34.38l-.486.087c-9.938-2.063-21.06-3.292-33.56-3.496-22.761-.373-40.026 5.967-53.127 15.902 0 0-161.411-66.495-153.904 83.63 1.597 31.938 45.776 241.657 98.471 178.312 19.26-23.163 37.869-42.748 37.869-42.748 9.243 6.14 20.308 9.272 31.908 8.147l.901-.765c-.28 2.876-.152 5.689.361 9.019-13.575 15.167-9.586 17.83-36.723 23.416-27.459 5.659-11.328 15.734-.796 18.367 12.768 3.193 42.307 7.716 62.266-20.224l-.796 3.188c5.319 4.26 9.054 27.711 8.428 48.969s-1.044 35.854 3.147 47.254 8.368 37.05 44.042 29.406c29.809-6.388 45.256-22.942 47.405-50.555 1.525-19.631 4.976-16.729 5.194-34.28l2.768-8.309c3.192-26.611.507-35.196 18.872-31.203l4.463.392c13.517.615 31.208-2.174 41.591-7 22.358-10.376 35.618-27.7 13.573-23.148z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M215.836 286.468c-1.385 49.516.348 99.377 5.193 111.495 4.848 12.118 15.223 35.688 50.9 28.045 29.806-6.39 40.651-18.756 45.357-46.051 3.466-20.082 10.148-75.854 11.005-87.281M173.074 38.24S11.553-27.776 19.062 122.349c1.597 31.938 45.779 241.664 98.473 178.316 19.256-23.166 36.671-41.335 36.671-41.335M260.319 26.191c-5.591 1.753 89.848-34.889 144.087 34.417 19.159 24.484-3.043 124.519-56.153 203.329"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="bevel"
|
||||
strokeWidth="12.465"
|
||||
d="M348.252 263.937s3.461 17.036 53.764 6.653c22.04-4.552 8.776 12.774-13.577 23.155-18.345 8.514-59.474 10.696-60.146-1.069-1.729-30.355 21.647-21.133 19.96-28.739-1.525-6.85-11.979-13.573-18.894-30.338-6.037-14.633-82.796-126.849 21.287-110.183 3.813-.789-27.146-99.002-124.553-100.599-97.385-1.597-94.19 119.762-94.19 119.762"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M188.574 274.318c-13.577 15.166-9.584 17.829-36.723 23.417-27.459 5.66-11.326 15.733-.797 18.365 12.768 3.195 42.307 7.718 62.266-20.229 6.078-8.509-.036-22.086-8.385-25.547-4.034-1.671-9.428-3.765-16.361 3.994"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M187.685 274.053c-1.368-8.917 2.93-19.528 7.536-31.942 6.922-18.626 22.893-37.255 10.117-96.339-9.523-44.029-73.396-9.163-73.436-3.193-.039 5.968 2.889 30.26-1.067 58.548-5.162 36.913 23.488 68.132 56.479 64.938"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
strokeWidth="4.155"
|
||||
d="M172.487 141.684c-.288 2.039 3.733 7.48 8.976 8.207 5.234.73 9.714-3.522 9.998-5.559.284-2.039-3.732-4.285-8.977-5.015-5.237-.731-9.719.333-9.996 2.367z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
strokeWidth="2.078"
|
||||
d="M331.911 137.527c.284 2.039-3.732 7.48-8.976 8.207-5.238.73-9.718-3.522-10.005-5.559-.277-2.039 3.74-4.285 8.979-5.015s9.718.333 10.002 2.368z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M350.646 123.416c.863 15.994-3.445 26.888-3.988 43.914-.804 24.748 11.799 53.074-7.191 81.435"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M503.964 133.681c8.155-1.357 18.819-2.523 32.409-2.523 16.677 0 28.907 3.882 36.669 10.867 7.178 6.206 11.447 15.714 11.447 27.362 0 11.836-3.499 21.148-10.097 27.939-8.925 9.506-23.475 14.357-39.968 14.357-5.043 0-9.701-.193-13.574-1.163v52.392h-16.886zm16.886 63.056c3.686.969 8.345 1.356 13.97 1.356 20.365 0 32.79-9.894 32.79-27.937 0-17.267-12.224-25.611-30.85-25.611-7.372 0-13 .583-15.91 1.359zm165.884 18.432c0 34.729-24.065 49.869-46.764 49.869-25.417 0-45.013-18.626-45.013-48.319 0-31.43 20.566-49.862 46.566-49.862 26.975-.007 45.211 19.596 45.211 48.312m-74.501.97c0 20.565 11.835 36.086 28.521 36.086 16.296 0 28.513-15.327 28.513-36.476 0-15.909-7.948-36.086-28.133-36.086-20.186 0-28.901 18.626-28.901 36.476m93.703 29.297c5.045 3.296 13.97 6.789 22.505 6.789 12.417 0 18.239-6.209 18.239-13.968 0-8.151-4.845-12.613-17.462-17.269-16.88-6.015-24.835-15.327-24.835-26.581 0-15.133 12.224-27.55 32.402-27.55 9.506 0 17.857 2.716 23.096 5.822l-4.269 12.417c-3.686-2.329-10.478-5.433-19.209-5.433-10.088 0-15.714 5.82-15.714 12.805 0 7.761 5.633 11.254 17.857 15.909 16.296 6.209 24.641 14.357 24.641 28.327 0 16.491-12.807 28.131-35.119 28.131-10.281 0-19.789-2.52-26.387-6.402zm99.732-103.421v26.971h24.438v12.997h-24.438v50.639c0 11.641 3.292 18.239 12.804 18.239 4.462 0 7.761-.583 9.897-1.166l.774 12.807c-3.29 1.357-8.536 2.326-15.124 2.326-7.955 0-14.366-2.522-18.439-7.178-4.843-5.043-6.598-13.387-6.598-24.445v-51.222h-14.551v-12.997h14.551v-22.508zm123.395 26.978c-.396 6.789-.776 14.356-.776 25.804v54.518c0 21.536-4.269 34.729-13.387 42.884-9.118 8.538-22.312 11.254-34.149 11.254-11.251 0-23.668-2.716-31.243-7.761l4.269-12.998c6.208 3.88 15.91 7.372 27.55 7.372 17.46 0 30.267-9.118 30.267-32.789v-10.478h-.389c-5.237 8.732-15.327 15.717-29.878 15.717-23.281 0-39.967-19.79-39.967-45.797 0-31.817 20.759-49.862 42.294-49.862 16.306 0 25.231 8.538 29.306 16.299h.387l.777-14.163zm-17.655 37.055c0-2.909-.194-5.432-.97-7.759-3.106-9.894-11.448-18.045-23.865-18.045-16.306 0-27.937 13.776-27.937 35.505 0 18.433 9.311 33.76 27.744 33.76 10.477 0 19.982-6.596 23.668-17.461.97-2.911 1.367-6.208 1.367-9.12v-16.88zm44.033-7.759c0-11.06-.193-20.565-.776-29.296h14.947l.576 18.429h.776c4.267-12.61 14.55-20.565 25.998-20.565 1.94 0 3.288.196 4.838.583v16.103c-1.73-.387-3.488-.583-5.808-.583-12.03 0-20.575 9.121-22.895 21.925-.387 2.329-.776 5.045-.776 7.955v50.056h-16.88zm71.019 20.759c.39 23.089 15.13 32.596 32.2 32.596 12.23 0 19.59-2.135 26-4.852l2.91 12.224c-6.02 2.716-16.3 5.819-31.24 5.819-28.91 0-46.18-19.012-46.18-47.339s16.69-50.646 44.05-50.646c30.65 0 38.8 26.968 38.8 44.237 0 3.492-.39 6.208-.58 7.954h-65.96zm50.06-12.223c.18-10.865-4.47-27.744-23.67-27.744-17.27 0-24.84 15.909-26.2 27.744z"
|
||||
/>
|
||||
<path
|
||||
fill="#336791"
|
||||
d="M1112.4 242.33c7.57 4.656 18.63 8.538 30.27 8.538 17.27 0 27.35-9.118 27.35-22.312 0-12.224-6.98-19.209-24.64-26-21.34-7.566-34.53-18.624-34.53-37.056 0-20.372 16.88-35.505 42.29-35.505 13.39 0 23.08 3.103 28.91 6.402l-4.65 13.776c-4.28-2.329-13-6.208-24.84-6.208-17.85 0-24.64 10.671-24.64 19.595 0 12.222 7.95 18.237 26 25.222 22.12 8.544 33.37 19.208 33.37 38.415 0 20.178-14.95 37.638-45.79 37.638-12.61 0-26.38-3.686-33.38-8.341zm205.86 39.774c-17.68-4.656-34.93-9.894-50.09-15.133-2.69-.97-5.39-1.94-7.94-1.94-31.04-1.165-57.62-24.058-57.62-66.161 0-41.908 25.61-68.875 60.94-68.875 35.5 0 58.57 27.55 58.57 66.159 0 33.565-15.5 55.101-37.23 62.669v.776c12.99 3.297 27.17 6.402 38.21 8.342zm-13.98-85.174c0-26.191-13.57-53.168-41.32-53.168-28.52 0-42.5 26.387-42.3 54.721-.2 27.744 15.14 52.772 41.52 52.772 26.98 0 42.1-24.445 42.1-54.325m39.77-64.802h16.87v116.604h55.89v14.164h-72.76z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { PostgresqlWordmarkDark };
|
||||
74
src/components/ui/svgs/postgresqlWordmarkLight.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const PostgresqlWordmarkLight = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 1416.8 445.5">
|
||||
<path
|
||||
stroke="#000"
|
||||
strokeWidth="37.395"
|
||||
d="M323.175 324.211c2.833-23.601 1.984-27.062 19.563-23.239l4.463.392c13.517.615 31.199-2.174 41.587-7 22.362-10.376 35.622-27.7 13.572-23.148-50.297 10.376-53.755-6.655-53.755-6.655 53.111-78.803 75.313-178.836 56.149-203.322-52.27-66.789-142.748-35.206-144.262-34.386l-.482.089c-9.938-2.062-21.06-3.294-33.554-3.496-22.761-.374-40.032 5.967-53.133 15.904 0 0-161.408-66.498-153.899 83.628 1.597 31.936 45.777 241.655 98.47 178.31 19.259-23.163 37.871-42.748 37.871-42.748 9.242 6.14 20.307 9.272 31.912 8.147l.897-.765c-.281 2.876-.157 5.689.359 9.019-13.572 15.167-9.584 17.83-36.723 23.416-27.457 5.659-11.326 15.734-.797 18.367 12.768 3.193 42.305 7.716 62.268-20.224l-.795 3.188c5.325 4.26 4.965 30.619 5.72 49.452.756 18.834 2.017 36.409 5.856 46.771 3.839 10.36 8.369 37.05 44.036 29.406 29.809-6.388 52.6-15.582 54.677-101.107"
|
||||
/>
|
||||
<path
|
||||
fill="#336791"
|
||||
d="M402.365 271.214c-50.302 10.376-53.76-6.655-53.76-6.655 53.111-78.808 75.313-178.843 56.153-203.326-52.27-66.785-142.752-35.2-144.262-34.38l-.486.087c-9.938-2.063-21.06-3.292-33.56-3.496-22.761-.373-40.026 5.967-53.127 15.902 0 0-161.411-66.495-153.904 83.63 1.597 31.938 45.776 241.657 98.471 178.312 19.26-23.163 37.869-42.748 37.869-42.748 9.243 6.14 20.308 9.272 31.908 8.147l.901-.765c-.28 2.876-.152 5.689.361 9.019-13.575 15.167-9.586 17.83-36.723 23.416-27.459 5.659-11.328 15.734-.796 18.367 12.768 3.193 42.307 7.716 62.266-20.224l-.796 3.188c5.319 4.26 9.054 27.711 8.428 48.969s-1.044 35.854 3.147 47.254 8.368 37.05 44.042 29.406c29.809-6.388 45.256-22.942 47.405-50.555 1.525-19.631 4.976-16.729 5.194-34.28l2.768-8.309c3.192-26.611.507-35.196 18.872-31.203l4.463.392c13.517.615 31.208-2.174 41.591-7 22.358-10.376 35.618-27.7 13.573-23.148z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M215.836 286.468c-1.385 49.516.348 99.377 5.193 111.495 4.848 12.118 15.223 35.688 50.9 28.045 29.806-6.39 40.651-18.756 45.357-46.051 3.466-20.082 10.148-75.854 11.005-87.281M173.074 38.24S11.553-27.776 19.062 122.349c1.597 31.938 45.779 241.664 98.473 178.316 19.256-23.166 36.671-41.335 36.671-41.335M260.319 26.191c-5.591 1.753 89.848-34.889 144.087 34.417 19.159 24.484-3.043 124.519-56.153 203.329"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="bevel"
|
||||
strokeWidth="12.465"
|
||||
d="M348.252 263.937s3.461 17.036 53.764 6.653c22.04-4.552 8.776 12.774-13.577 23.155-18.345 8.514-59.474 10.696-60.146-1.069-1.729-30.355 21.647-21.133 19.96-28.739-1.525-6.85-11.979-13.573-18.894-30.338-6.037-14.633-82.796-126.849 21.287-110.183 3.813-.789-27.146-99.002-124.553-100.599-97.385-1.597-94.19 119.762-94.19 119.762"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M188.574 274.318c-13.577 15.166-9.584 17.829-36.723 23.417-27.459 5.66-11.326 15.733-.797 18.365 12.768 3.195 42.307 7.718 62.266-20.229 6.078-8.509-.036-22.086-8.385-25.547-4.034-1.671-9.428-3.765-16.361 3.994"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M187.685 274.053c-1.368-8.917 2.93-19.528 7.536-31.942 6.922-18.626 22.893-37.255 10.117-96.339-9.523-44.029-73.396-9.163-73.436-3.193-.039 5.968 2.889 30.26-1.067 58.548-5.162 36.913 23.488 68.132 56.479 64.938"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
strokeWidth="4.155"
|
||||
d="M172.487 141.684c-.288 2.039 3.733 7.48 8.976 8.207 5.234.73 9.714-3.522 9.998-5.559.284-2.039-3.732-4.285-8.977-5.015-5.237-.731-9.719.333-9.996 2.367z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
strokeWidth="2.078"
|
||||
d="M331.911 137.527c.284 2.039-3.732 7.48-8.976 8.207-5.238.73-9.718-3.522-10.005-5.559-.277-2.039 3.74-4.285 8.979-5.015s9.718.333 10.002 2.368z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="12.465"
|
||||
d="M350.646 123.416c.863 15.994-3.445 26.888-3.988 43.914-.804 24.748 11.799 53.074-7.191 81.435"
|
||||
/>
|
||||
<path d="M503.964 133.681c8.155-1.357 18.819-2.523 32.409-2.523 16.677 0 28.907 3.882 36.669 10.867 7.178 6.206 11.447 15.714 11.447 27.362 0 11.836-3.499 21.148-10.097 27.939-8.925 9.506-23.475 14.357-39.968 14.357-5.043 0-9.701-.193-13.574-1.163v52.392h-16.886zm16.886 63.056c3.686.969 8.345 1.356 13.97 1.356 20.365 0 32.79-9.894 32.79-27.937 0-17.267-12.224-25.611-30.85-25.611-7.372 0-13 .583-15.91 1.359zm165.884 18.432c0 34.729-24.065 49.869-46.764 49.869-25.417 0-45.013-18.626-45.013-48.319 0-31.43 20.566-49.862 46.566-49.862 26.975-.007 45.211 19.596 45.211 48.312m-74.501.97c0 20.565 11.835 36.086 28.521 36.086 16.296 0 28.513-15.327 28.513-36.476 0-15.909-7.948-36.086-28.133-36.086-20.186 0-28.901 18.626-28.901 36.476m93.703 29.297c5.045 3.296 13.97 6.789 22.505 6.789 12.417 0 18.239-6.209 18.239-13.968 0-8.151-4.845-12.613-17.462-17.269-16.88-6.015-24.835-15.327-24.835-26.581 0-15.133 12.224-27.55 32.402-27.55 9.506 0 17.857 2.716 23.096 5.822l-4.269 12.417c-3.686-2.329-10.478-5.433-19.209-5.433-10.088 0-15.714 5.82-15.714 12.805 0 7.761 5.633 11.254 17.857 15.909 16.296 6.209 24.641 14.357 24.641 28.327 0 16.491-12.807 28.131-35.119 28.131-10.281 0-19.789-2.52-26.387-6.402zm99.732-103.421v26.971h24.438v12.997h-24.438v50.639c0 11.641 3.292 18.239 12.804 18.239 4.462 0 7.761-.583 9.897-1.166l.774 12.807c-3.29 1.357-8.536 2.326-15.124 2.326-7.955 0-14.366-2.522-18.439-7.178-4.843-5.043-6.598-13.387-6.598-24.445v-51.222h-14.551v-12.997h14.551v-22.508zm123.395 26.978c-.396 6.789-.776 14.356-.776 25.804v54.518c0 21.536-4.269 34.729-13.387 42.884-9.118 8.538-22.312 11.254-34.149 11.254-11.251 0-23.668-2.716-31.243-7.761l4.269-12.998c6.208 3.88 15.91 7.372 27.55 7.372 17.46 0 30.267-9.118 30.267-32.789v-10.478h-.389c-5.237 8.732-15.327 15.717-29.878 15.717-23.281 0-39.967-19.79-39.967-45.797 0-31.817 20.759-49.862 42.294-49.862 16.306 0 25.231 8.538 29.306 16.299h.387l.777-14.163zm-17.655 37.055c0-2.909-.194-5.432-.97-7.759-3.106-9.894-11.448-18.045-23.865-18.045-16.306 0-27.937 13.776-27.937 35.505 0 18.433 9.311 33.76 27.744 33.76 10.477 0 19.982-6.596 23.668-17.461.97-2.911 1.367-6.208 1.367-9.12v-16.88zm44.033-7.759c0-11.06-.193-20.565-.776-29.296h14.947l.576 18.429h.776c4.267-12.61 14.55-20.565 25.998-20.565 1.94 0 3.288.196 4.838.583v16.103c-1.73-.387-3.488-.583-5.808-.583-12.03 0-20.575 9.121-22.895 21.925-.387 2.329-.776 5.045-.776 7.955v50.056h-16.88zm71.019 20.759c.39 23.089 15.13 32.596 32.2 32.596 12.23 0 19.59-2.135 26-4.852l2.91 12.224c-6.02 2.716-16.3 5.819-31.24 5.819-28.91 0-46.18-19.012-46.18-47.339s16.69-50.646 44.05-50.646c30.65 0 38.8 26.968 38.8 44.237 0 3.492-.39 6.208-.58 7.954h-65.96zm50.06-12.223c.18-10.865-4.47-27.744-23.67-27.744-17.27 0-24.84 15.909-26.2 27.744z" />
|
||||
<path
|
||||
fill="#336791"
|
||||
d="M1112.4 242.33c7.57 4.656 18.63 8.538 30.27 8.538 17.27 0 27.35-9.118 27.35-22.312 0-12.224-6.98-19.209-24.64-26-21.34-7.566-34.53-18.624-34.53-37.056 0-20.372 16.88-35.505 42.29-35.505 13.39 0 23.08 3.103 28.91 6.402l-4.65 13.776c-4.28-2.329-13-6.208-24.84-6.208-17.85 0-24.64 10.671-24.64 19.595 0 12.222 7.95 18.237 26 25.222 22.12 8.544 33.37 19.208 33.37 38.415 0 20.178-14.95 37.638-45.79 37.638-12.61 0-26.38-3.686-33.38-8.341zm205.86 39.774c-17.68-4.656-34.93-9.894-50.09-15.133-2.69-.97-5.39-1.94-7.94-1.94-31.04-1.165-57.62-24.058-57.62-66.161 0-41.908 25.61-68.875 60.94-68.875 35.5 0 58.57 27.55 58.57 66.159 0 33.565-15.5 55.101-37.23 62.669v.776c12.99 3.297 27.17 6.402 38.21 8.342zm-13.98-85.174c0-26.191-13.57-53.168-41.32-53.168-28.52 0-42.5 26.387-42.3 54.721-.2 27.744 15.14 52.772 41.52 52.772 26.98 0 42.1-24.445 42.1-54.325m39.77-64.802h16.87v116.604h55.89v14.164h-72.76z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { PostgresqlWordmarkLight };
|
||||
40
src/components/ui/svgs/python.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Python = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} fill="none" viewBox="16 16 32 32">
|
||||
<path
|
||||
fill="url(#a)"
|
||||
d="M31.885 16c-8.124 0-7.617 3.523-7.617 3.523l.01 3.65h7.752v1.095H21.197S16 23.678 16 31.876c0 8.196 4.537 7.906 4.537 7.906h2.708v-3.804s-.146-4.537 4.465-4.537h7.688s4.32.07 4.32-4.175v-7.019S40.374 16 31.885 16zm-4.275 2.454a1.394 1.394 0 1 1 0 2.79 1.393 1.393 0 0 1-1.395-1.395c0-.771.624-1.395 1.395-1.395z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M32.115 47.833c8.124 0 7.617-3.523 7.617-3.523l-.01-3.65H31.97v-1.095h10.832S48 40.155 48 31.958c0-8.197-4.537-7.906-4.537-7.906h-2.708v3.803s.146 4.537-4.465 4.537h-7.688s-4.32-.07-4.32 4.175v7.019s-.656 4.247 7.833 4.247zm4.275-2.454a1.393 1.393 0 0 1-1.395-1.395 1.394 1.394 0 1 1 1.395 1.395z"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="19.075"
|
||||
x2="34.898"
|
||||
y1="18.782"
|
||||
y2="34.658"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#387EB8" />
|
||||
<stop offset="1" stopColor="#366994" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="28.809"
|
||||
x2="45.803"
|
||||
y1="28.882"
|
||||
y2="45.163"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FFE052" />
|
||||
<stop offset="1" stopColor="#FFC331" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Python };
|
||||
26
src/components/ui/svgs/reactDark.tsx
Normal file
24
src/components/ui/svgs/reactLight.tsx
Normal file
41
src/components/ui/svgs/reactWordmarkDark.tsx
Normal file
28
src/components/ui/svgs/reactWordmarkLight.tsx
Normal file
16
src/components/ui/svgs/typescript.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const Typescript = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 256 256" preserveAspectRatio="xMidYMid">
|
||||
<path
|
||||
d="M20 0h216c11.046 0 20 8.954 20 20v216c0 11.046-8.954 20-20 20H20c-11.046 0-20-8.954-20-20V20C0 8.954 8.954 0 20 0Z"
|
||||
fill="#3178C6"
|
||||
/>
|
||||
<path
|
||||
d="M150.518 200.475v27.62c4.492 2.302 9.805 4.028 15.938 5.179 6.133 1.151 12.597 1.726 19.393 1.726 6.622 0 12.914-.633 18.874-1.899 5.96-1.266 11.187-3.352 15.678-6.257 4.492-2.906 8.048-6.704 10.669-11.394 2.62-4.689 3.93-10.486 3.93-17.391 0-5.006-.749-9.394-2.246-13.163a30.748 30.748 0 0 0-6.479-10.055c-2.821-2.935-6.205-5.567-10.149-7.898-3.945-2.33-8.394-4.531-13.347-6.602-3.628-1.497-6.881-2.949-9.761-4.359-2.879-1.41-5.327-2.848-7.342-4.316-2.016-1.467-3.571-3.021-4.665-4.661-1.094-1.64-1.641-3.495-1.641-5.567 0-1.899.489-3.61 1.468-5.135s2.362-2.834 4.147-3.927c1.785-1.094 3.973-1.942 6.565-2.547 2.591-.604 5.471-.906 8.638-.906 2.304 0 4.737.173 7.299.518 2.563.345 5.14.877 7.732 1.597a53.669 53.669 0 0 1 7.558 2.719 41.7 41.7 0 0 1 6.781 3.797v-25.807c-4.204-1.611-8.797-2.805-13.778-3.582-4.981-.777-10.697-1.165-17.147-1.165-6.565 0-12.784.705-18.658 2.115-5.874 1.409-11.043 3.61-15.506 6.602-4.463 2.993-7.99 6.805-10.582 11.437-2.591 4.632-3.887 10.17-3.887 16.615 0 8.228 2.375 15.248 7.127 21.06 4.751 5.811 11.963 10.731 21.638 14.759a291.458 291.458 0 0 1 10.625 4.575c3.283 1.496 6.119 3.049 8.509 4.66 2.39 1.611 4.276 3.366 5.658 5.265 1.382 1.899 2.073 4.057 2.073 6.474a9.901 9.901 0 0 1-1.296 4.963c-.863 1.524-2.174 2.848-3.93 3.97-1.756 1.122-3.945 1.999-6.565 2.632-2.62.633-5.687.95-9.2.95-5.989 0-11.92-1.05-17.794-3.151-5.875-2.1-11.317-5.25-16.327-9.451Zm-46.036-68.733H140V109H41v22.742h35.345V233h28.137V131.742Z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Typescript };
|
||||
42
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
const TooltipArrow = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Arrow>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TooltipPrimitive.Arrow
|
||||
ref={ref}
|
||||
className={cn("fill-primary", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TooltipArrow };
|
||||
701
src/data/resume.tsx
Normal file
@@ -0,0 +1,701 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { HomeIcon, NotebookIcon } from "lucide-react";
|
||||
import { ReactLight } from "@/components/ui/svgs/reactLight";
|
||||
import { NextjsIconDark } from "@/components/ui/svgs/nextjsIconDark";
|
||||
import { Typescript } from "@/components/ui/svgs/typescript";
|
||||
import { Nodejs } from "@/components/ui/svgs/nodejs";
|
||||
import { Python } from "@/components/ui/svgs/python";
|
||||
import { Golang } from "@/components/ui/svgs/golang";
|
||||
import { Postgresql } from "@/components/ui/svgs/postgresql";
|
||||
import { Docker } from "@/components/ui/svgs/docker";
|
||||
import { Kubernetes } from "@/components/ui/svgs/kubernetes";
|
||||
import { Java } from "@/components/ui/svgs/java";
|
||||
import { Csharp } from "@/components/ui/svgs/csharp";
|
||||
|
||||
export const DATA = {
|
||||
name: "Dillion Verma",
|
||||
initials: "DV",
|
||||
url: "https://dillion.io",
|
||||
location: "San Francisco, CA",
|
||||
locationLink: "https://www.google.com/maps/place/sanfrancisco",
|
||||
description:
|
||||
"Software Engineer turned Entrepreneur. I love building things and helping people. Very active on Twitter.",
|
||||
summary:
|
||||
"At the end of 2022, I quit my job as a software engineer to go fulltime into building and scaling my own SaaS businesses. In the past, [I pursued a double degree in computer science and business](/#education), [interned at big tech companies in Silicon Valley](https://www.youtube.com/watch?v=d-LJ2e5qKdE), and [competed in over 21 hackathons for fun](/#hackathons). I also had the pleasure of being a part of the first ever in-person cohort of buildspace called [buildspace sf1](https://buildspace.so/sf1).",
|
||||
avatarUrl: "/me.png",
|
||||
skills: [
|
||||
{ name: "React", icon: ReactLight },
|
||||
{ name: "Next.js", icon: NextjsIconDark },
|
||||
{ name: "Typescript", icon: Typescript },
|
||||
{ name: "Node.js", icon: Nodejs },
|
||||
{ name: "Python", icon: Python },
|
||||
{ name: "Go", icon: Golang },
|
||||
{ name: "Postgres", icon: Postgresql },
|
||||
{ name: "Docker", icon: Docker },
|
||||
{ name: "Kubernetes", icon: Kubernetes },
|
||||
{ name: "Java", icon: Java },
|
||||
{ name: "C++", icon: Csharp },
|
||||
],
|
||||
navbar: [
|
||||
{ href: "/", icon: HomeIcon, label: "Home" },
|
||||
{ href: "/blog", icon: NotebookIcon, label: "Blog" },
|
||||
],
|
||||
contact: {
|
||||
email: "hello@example.com",
|
||||
tel: "+123456789",
|
||||
social: {
|
||||
GitHub: {
|
||||
name: "GitHub",
|
||||
url: "https://dub.sh/dillion-github",
|
||||
icon: Icons.github,
|
||||
navbar: true,
|
||||
},
|
||||
|
||||
LinkedIn: {
|
||||
name: "LinkedIn",
|
||||
url: "https://dub.sh/dillion-linkedin",
|
||||
icon: Icons.linkedin,
|
||||
|
||||
navbar: true,
|
||||
},
|
||||
X: {
|
||||
name: "X",
|
||||
url: "https://dub.sh/dillion-twitter",
|
||||
icon: Icons.x,
|
||||
|
||||
navbar: true,
|
||||
},
|
||||
Youtube: {
|
||||
name: "Youtube",
|
||||
url: "https://dub.sh/dillion-youtube",
|
||||
icon: Icons.youtube,
|
||||
navbar: true,
|
||||
},
|
||||
email: {
|
||||
name: "Send Email",
|
||||
url: "#",
|
||||
icon: Icons.email,
|
||||
|
||||
navbar: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
work: [
|
||||
{
|
||||
company: "Atomic Finance",
|
||||
href: "https://atomic.finance",
|
||||
badges: [],
|
||||
location: "Remote",
|
||||
title: "Bitcoin Protocol Engineer",
|
||||
logoUrl: "/atomic.png",
|
||||
start: "May 2021",
|
||||
end: "Oct 2022",
|
||||
description:
|
||||
"Implemented the Bitcoin discreet log contract (DLC) protocol specifications as an open source Typescript SDK. Dockerized all microservices and setup production kubernetes cluster. Architected a data lake using AWS S3 and Athena for historical backtesting of bitcoin trading strategies. Built a mobile app using react native and typescript.",
|
||||
},
|
||||
{
|
||||
company: "Shopify",
|
||||
badges: [],
|
||||
href: "https://shopify.com",
|
||||
location: "Remote",
|
||||
title: "Software Engineer",
|
||||
logoUrl: "/shopify.svg",
|
||||
start: "January 2021",
|
||||
end: "April 2021",
|
||||
description:
|
||||
"Implemented a custom Kubernetes controller in Go to automate the deployment of MySQL and ProxySQL custom resources in order to enable 2,000+ internal developers to instantly deploy their app databases to production. Wrote several scripts in Go to automate MySQL database failovers while maintaining master-slave replication topologies and keeping Zookeeper nodes consistent with changes.",
|
||||
},
|
||||
{
|
||||
company: "Nvidia",
|
||||
href: "https://nvidia.com/",
|
||||
badges: [],
|
||||
location: "Santa Clara, CA",
|
||||
title: "Software Engineer",
|
||||
logoUrl: "/nvidia.png",
|
||||
start: "January 2020",
|
||||
end: "April 2020",
|
||||
description:
|
||||
"Architected and wrote the entire MVP of the GeForce Now Cloud Gaming internal admin and A/B testing dashboard using React, Redux, TypeScript, and Python.",
|
||||
},
|
||||
{
|
||||
company: "Splunk",
|
||||
href: "https://splunk.com",
|
||||
badges: [],
|
||||
location: "San Jose, CA",
|
||||
title: "Software Engineer",
|
||||
logoUrl: "/splunk.svg",
|
||||
start: "January 2019",
|
||||
end: "April 2019",
|
||||
description:
|
||||
"Co-developed a prototype iOS app with another intern in Swift for the new Splunk Phantom security orchestration product (later publicly demoed and launched at .conf annual conference in Las Vegas). Implemented a realtime service for the iOS app in Django (Python) and C++; serialized data using protobufs transmitted over gRPC resulting in an approximate 500% increase in data throughput.",
|
||||
},
|
||||
{
|
||||
company: "Lime",
|
||||
href: "https://li.me/",
|
||||
badges: [],
|
||||
location: "San Francisco, CA",
|
||||
title: "Software Engineer",
|
||||
logoUrl: "/lime.svg",
|
||||
start: "January 2018",
|
||||
end: "April 2018",
|
||||
description:
|
||||
"Proposed and implemented an internal ruby API for sending/receiving commands to scooters over LTE networks. Developed a fully automated bike firmware update system to handle asynchronous firmware updates of over 100,000+ scooters worldwide, and provide progress reports in real-time using React, Ruby on Rails, PostgreSQL and AWS EC2 saving hundreds of developer hours.",
|
||||
},
|
||||
{
|
||||
company: "Mitre Media",
|
||||
href: "https://mitremedia.com/",
|
||||
badges: [],
|
||||
location: "Toronto, ON",
|
||||
title: "Software Engineer",
|
||||
logoUrl: "/mitremedia.png",
|
||||
start: "May 2017",
|
||||
end: "August 2017",
|
||||
description:
|
||||
"Designed and implemented a robust password encryption and browser cookie storage system in Ruby on Rails. Leveraged the Yahoo finance API to develop the dividend.com equity screener",
|
||||
},
|
||||
],
|
||||
education: [
|
||||
{
|
||||
school: "Buildspace",
|
||||
href: "https://buildspace.so",
|
||||
degree: "s3, s4, sf1, s5",
|
||||
logoUrl: "/buildspace.jpg",
|
||||
start: "2023",
|
||||
end: "2024",
|
||||
},
|
||||
{
|
||||
school: "University of Waterloo",
|
||||
href: "https://uwaterloo.ca",
|
||||
degree: "Bachelor's Degree of Computer Science (BCS)",
|
||||
logoUrl: "/waterloo.png",
|
||||
start: "2016",
|
||||
end: "2021",
|
||||
},
|
||||
{
|
||||
school: "Wilfrid Laurier University",
|
||||
href: "https://wlu.ca",
|
||||
degree: "Bachelor's Degree of Business Administration (BBA)",
|
||||
logoUrl: "/laurier.png",
|
||||
start: "2016",
|
||||
end: "2021",
|
||||
},
|
||||
{
|
||||
school: "International Baccalaureate",
|
||||
href: "https://ibo.org",
|
||||
degree: "IB Diploma",
|
||||
logoUrl: "/ib.png",
|
||||
start: "2012",
|
||||
end: "2016",
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
title: "Chat Collect",
|
||||
href: "https://chatcollect.com",
|
||||
dates: "Jan 2024 - Feb 2024",
|
||||
active: true,
|
||||
description:
|
||||
"With the release of the [OpenAI GPT Store](https://openai.com/blog/introducing-the-gpt-store), I decided to build a SaaS which allows users to collect email addresses from their GPT users. This is a great way to build an audience and monetize your GPT API usage.",
|
||||
technologies: [
|
||||
"Next.js",
|
||||
"Typescript",
|
||||
"PostgreSQL",
|
||||
"Prisma",
|
||||
"TailwindCSS",
|
||||
"Stripe",
|
||||
"Shadcn UI",
|
||||
"Magic UI",
|
||||
],
|
||||
links: [
|
||||
{
|
||||
type: "Website",
|
||||
href: "https://chatcollect.com",
|
||||
icon: <Icons.globe className="size-3" />,
|
||||
},
|
||||
],
|
||||
image: "",
|
||||
video:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/chat-collect.mp4",
|
||||
},
|
||||
{
|
||||
title: "Magic UI",
|
||||
href: "https://magicui.design",
|
||||
dates: "June 2023 - Present",
|
||||
active: true,
|
||||
description:
|
||||
"Designed, developed and sold animated UI components for developers.",
|
||||
technologies: [
|
||||
"Next.js",
|
||||
"Typescript",
|
||||
"PostgreSQL",
|
||||
"Prisma",
|
||||
"TailwindCSS",
|
||||
"Stripe",
|
||||
"Shadcn UI",
|
||||
"Magic UI",
|
||||
],
|
||||
links: [
|
||||
{
|
||||
type: "Website",
|
||||
href: "https://magicui.design",
|
||||
icon: <Icons.globe className="size-3" />,
|
||||
},
|
||||
{
|
||||
type: "Source",
|
||||
href: "https://github.com/magicuidesign/magicui",
|
||||
icon: <Icons.github className="size-3" />,
|
||||
},
|
||||
],
|
||||
image: "",
|
||||
video: "https://cdn.magicui.design/bento-grid.mp4",
|
||||
},
|
||||
{
|
||||
title: "llm.report",
|
||||
href: "https://llm.report",
|
||||
dates: "April 2023 - September 2023",
|
||||
active: true,
|
||||
description:
|
||||
"Developed an open-source logging and analytics platform for OpenAI: Log your ChatGPT API requests, analyze costs, and improve your prompts.",
|
||||
technologies: [
|
||||
"Next.js",
|
||||
"Typescript",
|
||||
"PostgreSQL",
|
||||
"Prisma",
|
||||
"TailwindCSS",
|
||||
"Shadcn UI",
|
||||
"Magic UI",
|
||||
"Stripe",
|
||||
"Cloudflare Workers",
|
||||
],
|
||||
links: [
|
||||
{
|
||||
type: "Website",
|
||||
href: "https://llm.report",
|
||||
icon: <Icons.globe className="size-3" />,
|
||||
},
|
||||
{
|
||||
type: "Source",
|
||||
href: "https://github.com/dillionverma/llm.report",
|
||||
icon: <Icons.github className="size-3" />,
|
||||
},
|
||||
],
|
||||
image: "",
|
||||
video: "https://cdn.llm.report/openai-demo.mp4",
|
||||
},
|
||||
{
|
||||
title: "Automatic Chat",
|
||||
href: "https://automatic.chat",
|
||||
dates: "April 2023 - March 2024",
|
||||
active: true,
|
||||
description:
|
||||
"Developed an AI Customer Support Chatbot which automatically responds to customer support tickets using the latest GPT models.",
|
||||
technologies: [
|
||||
"Next.js",
|
||||
"Typescript",
|
||||
"PostgreSQL",
|
||||
"Prisma",
|
||||
"TailwindCSS",
|
||||
"Shadcn UI",
|
||||
"Magic UI",
|
||||
"Stripe",
|
||||
"Cloudflare Workers",
|
||||
],
|
||||
links: [
|
||||
{
|
||||
type: "Website",
|
||||
href: "https://automatic.chat",
|
||||
icon: <Icons.globe className="size-3" />,
|
||||
},
|
||||
],
|
||||
image: "",
|
||||
video:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/automatic-chat.mp4",
|
||||
},
|
||||
],
|
||||
hackathons: [
|
||||
{
|
||||
title: "Hack Western 5",
|
||||
dates: "November 23rd - 25th, 2018",
|
||||
location: "London, Ontario",
|
||||
description:
|
||||
"Developed a mobile application which delivered bedtime stories to children using augmented reality.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/hack-western.png",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2019/mlh-trust-badge-2019-white.svg",
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: "Hack The North",
|
||||
dates: "September 14th - 16th, 2018",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed a mobile application which delivers university campus wide events in real time to all students.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/hack-the-north.png",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2019/mlh-trust-badge-2019-white.svg",
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: "FirstNet Public Safety Hackathon",
|
||||
dates: "March 23rd - 24th, 2018",
|
||||
location: "San Francisco, California",
|
||||
description:
|
||||
"Developed a mobile application which communcicates a victims medical data from inside an ambulance to doctors at hospital.",
|
||||
icon: "public",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/firstnet.png",
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: "DeveloperWeek Hackathon",
|
||||
dates: "February 3rd - 4th, 2018",
|
||||
location: "San Francisco, California",
|
||||
description:
|
||||
"Developed a web application which aggregates social media data regarding cryptocurrencies and predicts future prices.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/developer-week.jpg",
|
||||
links: [
|
||||
{
|
||||
title: "Github",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/cryptotrends/cryptotrends",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "HackDavis",
|
||||
dates: "January 20th - 21st, 2018",
|
||||
location: "Davis, California",
|
||||
description:
|
||||
"Developed a mobile application which allocates a daily carbon emission allowance to users to move towards a sustainable environment.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/hack-davis.png",
|
||||
win: "Best Data Hack",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2018/white.svg",
|
||||
links: [
|
||||
{
|
||||
title: "Devpost",
|
||||
icon: <Icons.globe className="h-4 w-4" />,
|
||||
href: "https://devpost.com/software/my6footprint",
|
||||
},
|
||||
{
|
||||
title: "ML",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/Wallet6/my6footprint-machine-learning",
|
||||
},
|
||||
{
|
||||
title: "iOS",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/Wallet6/CarbonWallet",
|
||||
},
|
||||
{
|
||||
title: "Server",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/Wallet6/wallet6-server",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "ETH Waterloo",
|
||||
dates: "October 13th - 15th, 2017",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed a blockchain application for doctors and pharmacists to perform trustless transactions and prevent overdosage in patients.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/eth-waterloo.png",
|
||||
links: [
|
||||
{
|
||||
title: "Organization",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/ethdocnet",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Hack The North",
|
||||
dates: "September 15th - 17th, 2017",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed a virtual reality application allowing users to see themselves in third person.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/hack-the-north.png",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2017/white.svg",
|
||||
links: [
|
||||
{
|
||||
title: "Streamer Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/justinmichaud/htn2017",
|
||||
},
|
||||
{
|
||||
title: "Client Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/RTSPClient",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Hack The 6ix",
|
||||
dates: "August 26th - 27th, 2017",
|
||||
location: "Toronto, Ontario",
|
||||
description:
|
||||
"Developed an open platform for people shipping items to same place to combine shipping costs and save money.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/hack-the-6ix.jpg",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2017/white.svg",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/ShareShip/ShareShip",
|
||||
},
|
||||
{
|
||||
title: "Site",
|
||||
icon: <Icons.globe className="h-4 w-4" />,
|
||||
href: "https://share-ship.herokuapp.com/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Stupid Hack Toronto",
|
||||
dates: "July 23rd, 2017",
|
||||
location: "Toronto, Ontario",
|
||||
description:
|
||||
"Developed a chrome extension which tracks which facebook profiles you have visited and immediately texts your girlfriend if you visited another girls page.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/stupid-hackathon.png",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/nsagirlfriend/nsagirlfriend",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Global AI Hackathon - Toronto",
|
||||
dates: "June 23rd - 25th, 2017",
|
||||
location: "Toronto, Ontario",
|
||||
description:
|
||||
"Developed a python library which can be imported to any python game and change difficulty of the game based on real time emotion of player. Uses OpenCV and webcam for facial recognition, and a custom Machine Learning Model trained on a [Kaggle Emotion Dataset](https://www.kaggle.com/c/challenges-in-representation-learning-facial-expression-recognition-challenge/leaderboard) using [Tensorflow](https://www.tensorflow.org/Tensorflow) and [Keras](https://keras.io/). This project recieved 1st place prize at the Global AI Hackathon - Toronto and was also invited to demo at [NextAI Canada](https://www.nextcanada.com/next-ai).",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/global-ai-hackathon.jpg",
|
||||
win: "1st Place Winner",
|
||||
links: [
|
||||
{
|
||||
title: "Article",
|
||||
icon: <Icons.globe className="h-4 w-4" />,
|
||||
href: "https://syncedreview.com/2017/06/26/global-ai-hackathon-in-toronto/",
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/TinySamosas/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "McGill AI for Social Innovation Hackathon",
|
||||
dates: "June 17th - 18th, 2017",
|
||||
location: "Montreal, Quebec",
|
||||
description:
|
||||
"Developed realtime facial microexpression analyzer using AI",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/ai-for-social-good.jpg",
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: "Open Source Circular Economy Days Hackathon",
|
||||
dates: "June 10th, 2017",
|
||||
location: "Toronto, Ontario",
|
||||
description:
|
||||
"Developed a custom admin interface for food waste startup <a href='http://genecis.co/'>Genecis</a> to manage their data and provide analytics.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/open-source-circular-economy-days.jpg",
|
||||
win: "1st Place Winner",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/genecis",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Make School's Student App Competition 2017",
|
||||
dates: "May 19th - 21st, 2017",
|
||||
location: "International",
|
||||
description: "Improved PocketDoc and submitted to online competition",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/make-school-hackathon.png",
|
||||
win: "Top 10 Finalist | Honourable Mention",
|
||||
links: [
|
||||
{
|
||||
title: "Medium Article",
|
||||
icon: <Icons.globe className="h-4 w-4" />,
|
||||
href: "https://medium.com/make-school/the-winners-of-make-schools-student-app-competition-2017-a6b0e72f190a",
|
||||
},
|
||||
{
|
||||
title: "Devpost",
|
||||
icon: <Icons.globe className="h-4 w-4" />,
|
||||
href: "https://devpost.com/software/pocketdoc-react-native",
|
||||
},
|
||||
{
|
||||
title: "YouTube",
|
||||
icon: <Icons.youtube className="h-4 w-4" />,
|
||||
href: "https://www.youtube.com/watch?v=XwFdn5Rmx68",
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/pocketdoc-react-native",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "HackMining",
|
||||
dates: "May 12th - 14th, 2017",
|
||||
location: "Toronto, Ontario",
|
||||
description: "Developed neural network to optimize a mining process",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/hack-mining.png",
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: "Waterloo Equithon",
|
||||
dates: "May 5th - 7th, 2017",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed Pocketdoc, an app in which you take a picture of a physical wound, and the app returns common solutions or cures to the injuries or diseases.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/waterloo-equithon.png",
|
||||
links: [
|
||||
{
|
||||
title: "Devpost",
|
||||
icon: <Icons.globe className="h-4 w-4" />,
|
||||
href: "https://devpost.com/software/pocketdoc-react-native",
|
||||
},
|
||||
{
|
||||
title: "YouTube",
|
||||
icon: <Icons.youtube className="h-4 w-4" />,
|
||||
href: "https://www.youtube.com/watch?v=XwFdn5Rmx68",
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/pocketdoc-react-native",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "SpaceApps Waterloo",
|
||||
dates: "April 28th - 30th, 2017",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed Earthwatch, a web application which allows users in a plane to virtually see important points of interest about the world below them. They can even choose to fly away from their route and then fly back if they choose. Special thanks to CesiumJS for providing open source world and plane models.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/space-apps.png",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/earthwatch",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "MHacks 9",
|
||||
dates: "March 24th - 26th, 2017",
|
||||
location: "Ann Arbor, Michigan",
|
||||
description:
|
||||
"Developed Super Graphic Air Traffic, a VR website made to introduce people to the world of air traffic controlling. This project was built completely using THREE.js as well as a node backend server.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/mhacks-9.png",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2017/white.svg",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/threejs-planes",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "StartHacks I",
|
||||
dates: "March 4th - 5th, 2017",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed at StartHacks 2017, Recipic is a mobile app which allows you to take pictures of ingredients around your house, and it will recognize those ingredients using ClarifAI image recognition API and return possible recipes to make. Recipic recieved 1st place at the hackathon for best pitch and hack.",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/starthacks.png",
|
||||
win: "1st Place Winner",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2017/white.svg",
|
||||
links: [
|
||||
{
|
||||
title: "Source (Mobile)",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/mattBlackDesign/recipic-ionic",
|
||||
},
|
||||
{
|
||||
title: "Source (Server)",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/mattBlackDesign/recipic-rails",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "QHacks II",
|
||||
dates: "February 3rd - 5th, 2017",
|
||||
location: "Kingston, Ontario",
|
||||
description:
|
||||
"Developed a mobile game which enables city-wide manhunt with random lobbies",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/qhacks.png",
|
||||
mlh: "https://s3.amazonaws.com/logged-assets/trust-badge/2017/white.svg",
|
||||
links: [
|
||||
{
|
||||
title: "Source (Mobile)",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/dillionverma/human-huntr-react-native",
|
||||
},
|
||||
{
|
||||
title: "Source (API)",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/mattBlackDesign/human-huntr-rails",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Terrible Hacks V",
|
||||
dates: "November 26th, 2016",
|
||||
location: "Waterloo, Ontario",
|
||||
description:
|
||||
"Developed a mock of Windows 11 with interesting notifications and functionality",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/terrible-hacks-v.png",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/justinmichaud/TerribleHacks2016-Windows11",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Portal Hackathon",
|
||||
dates: "October 29, 2016",
|
||||
location: "Kingston, Ontario",
|
||||
description:
|
||||
"Developed an internal widget for uploading assignments using Waterloo's portal app",
|
||||
image:
|
||||
"https://pub-83c5db439b40468498f97946200806f7.r2.dev/hackline/portal-hackathon.png",
|
||||
links: [
|
||||
{
|
||||
title: "Source",
|
||||
icon: <Icons.github className="h-4 w-4" />,
|
||||
href: "https://github.com/UWPortalSDK/crowmark",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
85
src/lib/pagination.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Pagination utilities for blog posts and other content collections
|
||||
*/
|
||||
|
||||
export interface PaginationOptions {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface PaginationResult<T> {
|
||||
items: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate an array of items
|
||||
*/
|
||||
export function paginate<T>(
|
||||
items: T[],
|
||||
options: PaginationOptions
|
||||
): PaginationResult<T> {
|
||||
const { page, pageSize } = options;
|
||||
const totalItems = items.length;
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
|
||||
const paginatedItems = items.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
items: paginatedItems,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
totalItems,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination metadata without slicing the array
|
||||
*/
|
||||
export function getPaginationMeta(
|
||||
totalItems: number,
|
||||
options: PaginationOptions
|
||||
) {
|
||||
const { page, pageSize } = options;
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
totalItems,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize page number
|
||||
*/
|
||||
export function normalizePage(page: number | string | undefined, maxPage: number): number {
|
||||
if (typeof page === "string") {
|
||||
const parsed = parseInt(page, 10);
|
||||
if (isNaN(parsed) || parsed < 1) return 1;
|
||||
return Math.min(parsed, maxPage);
|
||||
}
|
||||
if (typeof page === "number") {
|
||||
if (page < 1) return 1;
|
||||
return Math.min(page, maxPage);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
31
src/lib/remark-code-meta.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export function remarkCodeMeta() {
|
||||
return (tree: any) => {
|
||||
const walk = (node: any) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
|
||||
if (node.type === "code") {
|
||||
const meta: string | undefined = node.meta;
|
||||
if (meta) {
|
||||
node.data ||= {};
|
||||
node.data.hProperties ||= {};
|
||||
|
||||
node.data.hProperties["data-meta"] = meta;
|
||||
|
||||
const titleMatch = meta.match(/title="([^"]+)"/);
|
||||
if (titleMatch?.[1]) {
|
||||
node.data.hProperties["data-title"] = titleMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const children = node.children;
|
||||
if (Array.isArray(children)) {
|
||||
for (const child of children) walk(child);
|
||||
}
|
||||
};
|
||||
|
||||
walk(tree);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
17
src/lib/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDate(date: string | Date) {
|
||||
// Use UTC to ensure consistent formatting between server and client
|
||||
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||
return dateObj.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
49
src/mdx-components.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { CodeBlock } from "@/components/mdx/code-block";
|
||||
import { MediaContainer } from "@/components/mdx/media-container";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
type CodeProps = ComponentProps<"code"> & {
|
||||
"data-language"?: string;
|
||||
};
|
||||
|
||||
export const mdxComponents = {
|
||||
MediaContainer,
|
||||
pre: (props: ComponentProps<"pre">) => <CodeBlock {...props} />,
|
||||
hr: (props: ComponentProps<"hr">) => (
|
||||
<div className="my-10 flex w-full items-center" {...props}>
|
||||
<div
|
||||
className="flex-1 h-px bg-border"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
table: (props: ComponentProps<"table">) => (
|
||||
<div className="my-6 border border-border rounded-xl overflow-hidden">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<table
|
||||
className="m-0! w-full min-w-full border-separate border-spacing-0"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
code: ({ children, ...props }: CodeProps) => {
|
||||
if (props["data-language"]) {
|
||||
return <code {...props}>{children}</code>;
|
||||
}
|
||||
return (
|
||||
<code
|
||||
className="px-1.5 py-0.5 rounded-md bg-muted/60 dark:bg-muted/40 text-sm font-mono text-foreground/90"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
} as const;
|
||||
|
||||
44
tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"content-collections": [
|
||||
"./.content-collections/generated"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||