initial commit
@@ -0,0 +1,9 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env*.local
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Portfolio Website
|
||||||
|
|
||||||
|
A modern, minimalist portfolio built with Next.js, TypeScript, and shadcn/ui. Features a clean dark theme design with static site generation for optimal performance.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework:** Next.js 16.0.10
|
||||||
|
- **Language:** TypeScript 5.9.3
|
||||||
|
- **UI Library:** React 19.2.1
|
||||||
|
- **Component Library:** shadcn/ui
|
||||||
|
- **Styling:** Tailwind CSS 4.1.18
|
||||||
|
- **Content:** Markdown files with gray-matter and react-markdown
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view the site.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
Create an optimized production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
Start the production server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── app/ # Next.js app directory
|
||||||
|
│ ├── page.tsx # Landing page
|
||||||
|
│ ├── about/ # About page
|
||||||
|
│ ├── blog/ # Blog pages
|
||||||
|
│ │ ├── page.tsx # Blog listing
|
||||||
|
│ │ └── [slug]/ # Individual blog posts
|
||||||
|
│ └── projects/ # Projects page
|
||||||
|
├── components/ # React components
|
||||||
|
│ ├── navigation.tsx # Site navigation
|
||||||
|
│ ├── blog-card.tsx # Blog post card
|
||||||
|
│ └── project-card.tsx # Project card
|
||||||
|
├── content/ # Markdown content
|
||||||
|
│ ├── blog/ # Blog post markdown files
|
||||||
|
│ └── projects/ # Project markdown files
|
||||||
|
└── lib/ # Utility functions
|
||||||
|
└── content.ts # Content parsing utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Content
|
||||||
|
|
||||||
|
### Blog Posts
|
||||||
|
|
||||||
|
Create a new markdown file in `content/blog/`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: 'Your Post Title'
|
||||||
|
date: '2025-12-12'
|
||||||
|
excerpt: 'A brief summary of your post'
|
||||||
|
---
|
||||||
|
|
||||||
|
Your post content here...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Projects
|
||||||
|
|
||||||
|
Create a new markdown file in `content/projects/`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: 'Your Project'
|
||||||
|
description: 'A brief description'
|
||||||
|
links:
|
||||||
|
github: 'https://github.com/username/repo'
|
||||||
|
live: 'https://example.com'
|
||||||
|
gitea: 'https://gitea.example.com/username/repo'
|
||||||
|
---
|
||||||
|
|
||||||
|
Detailed project description...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Personal Information
|
||||||
|
|
||||||
|
Update the following files with your information:
|
||||||
|
|
||||||
|
- `app/page.tsx` - Update the welcome message and name
|
||||||
|
- `app/about/page.tsx` - Update bio, photo, and social links
|
||||||
|
- `app/layout.tsx` - Update site metadata
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
The site uses a dark theme by default. To customize colors, edit `app/globals.css` and modify the CSS variables in the `.dark` class.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
All dependencies are pinned to exact versions for reproducible builds. To update dependencies, modify `package.json` and run `npm install`.
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
This project includes Docker support for easy deployment with Traefik.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Traefik running in your environment
|
||||||
|
- A `traefik` external network created
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Update the domain in `compose.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.portfolio.rule=Host(`your-domain.com`)"
|
||||||
|
- "traefik.http.routers.portfolio-secure.rule=Host(`your-domain.com`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build and start the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. View logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traefik Configuration
|
||||||
|
|
||||||
|
The compose file includes:
|
||||||
|
|
||||||
|
- Automatic HTTP to HTTPS redirect
|
||||||
|
- Let's Encrypt SSL certificate generation
|
||||||
|
- Load balancer configuration for port 3000
|
||||||
|
- Dockge labels for better organization
|
||||||
|
|
||||||
|
### Using with Dockge
|
||||||
|
|
||||||
|
Simply import the `compose.yaml` file into Dockge, or place this project directory in your Dockge stacks folder. The service will appear with the metadata labels for easy identification.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is open source and available under the MIT License.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
import { Github, Linkedin, Mail, Code } from "lucide-react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "About",
|
||||||
|
description: "Learn more about me and my work",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navigation />
|
||||||
|
<main className="container mx-auto px-6 py-16 max-w-4xl">
|
||||||
|
<h1 className="text-4xl font-bold text-foreground mb-12">About Me</h1>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-[1fr_auto] gap-12 mb-12">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||||
|
I'm Alex, a full-stack engineer at Whisker with 4 years of
|
||||||
|
experience building scalable cloud applications and IoT solutions.
|
||||||
|
My journey in software development has been driven by curiosity
|
||||||
|
and a passion for solving complex problems with efficient
|
||||||
|
solutions. I specialize in creating performant applications using
|
||||||
|
the latest technologies, with a particular interest in performance
|
||||||
|
at scale and building developer-friendly tools.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||||
|
Learning and sharing new tech is something I'm genuinely
|
||||||
|
passionate about. When I'm not coding, you'll find me contributing
|
||||||
|
to open-source projects, writing blog posts, or giving technical
|
||||||
|
talks. Off-duty, I'm usually reading, exploring the outdoors with
|
||||||
|
my dogs, or rolling dice as a D&D sorcerer. This site is where
|
||||||
|
I share book thoughts, code experiments, and random shower
|
||||||
|
thoughts. Thanks for stopping by!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex md:flex-col gap-6 items-center md:items-start">
|
||||||
|
<div className="relative w-80 h-80 rounded-lg overflow-hidden bg-muted">
|
||||||
|
<img
|
||||||
|
src="/self.webp"
|
||||||
|
alt="Alex"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-80 h-80 rounded-lg overflow-hidden bg-muted">
|
||||||
|
<img
|
||||||
|
src="/homies.jpg"
|
||||||
|
alt="Err"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border/40 pt-12">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-8">
|
||||||
|
Connect With Me
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<a
|
||||||
|
href="https://github.com/amuszyn"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-lg border border-border/40 bg-card hover:border-border hover:shadow-lg transition-all group"
|
||||||
|
>
|
||||||
|
<Github
|
||||||
|
size={28}
|
||||||
|
className="text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-foreground">GitHub</div>
|
||||||
|
<div className="text-sm text-muted-foreground">@amuszyn</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://linkedin.com/in/almuszynski"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-lg border border-border/40 bg-card hover:border-border hover:shadow-lg transition-all group"
|
||||||
|
>
|
||||||
|
<Linkedin
|
||||||
|
size={28}
|
||||||
|
className="text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-foreground">LinkedIn</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Alex Muszynski
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:alexander.muszynski@gmail.com"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-lg border border-border/40 bg-card hover:border-border hover:shadow-lg transition-all group"
|
||||||
|
>
|
||||||
|
<Mail
|
||||||
|
size={28}
|
||||||
|
className="text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-foreground">Email</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
alexander.muszynski@gmail.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://gitea.muszyn.dev/muszyn"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-lg border border-border/40 bg-card hover:border-border hover:shadow-lg transition-all group"
|
||||||
|
>
|
||||||
|
<Code
|
||||||
|
size={28}
|
||||||
|
className="text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-foreground">Gitea</div>
|
||||||
|
<div className="text-sm text-muted-foreground">@muszyn</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
import { getBlogPosts, getBlogPost } from "@/lib/content";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const posts = getBlogPosts();
|
||||||
|
return posts.map((post) => ({
|
||||||
|
slug: post.slug,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: BlogPostPageProps): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getBlogPost(slug);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return {
|
||||||
|
title: "Post Not Found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${post.title}`,
|
||||||
|
description: post.excerpt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPostPage({ params }: BlogPostPageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getBlogPost(slug);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navigation />
|
||||||
|
<main className="container mx-auto px-6 py-16 max-w-3xl">
|
||||||
|
<article>
|
||||||
|
<header className="mb-10">
|
||||||
|
<time className="text-sm text-muted-foreground">
|
||||||
|
{new Date(post.date).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
<h1 className="text-4xl font-bold text-foreground mt-3 mb-4">
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-muted-foreground">{post.excerpt}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="prose prose-invert prose-lg max-w-none">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{post.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
import { BlogCard } from "@/components/blog-card";
|
||||||
|
import { getBlogPosts } from "@/lib/content";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blog",
|
||||||
|
description: "Read my latest blog posts about web development",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BlogPage() {
|
||||||
|
const posts = getBlogPosts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navigation />
|
||||||
|
<main className="container mx-auto px-6 py-16 max-w-4xl">
|
||||||
|
<div className="mb-16">
|
||||||
|
<h1 className="text-4xl font-bold text-foreground mb-4">Blog</h1>
|
||||||
|
<p className="text-lg text-muted-foreground">
|
||||||
|
Thoughts, tutorials, and insights about web development
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-12">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<BlogCard key={post.slug} post={post} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,185 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
: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.145 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 outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.prose {
|
||||||
|
@apply text-foreground;
|
||||||
|
max-width: 65ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1 {
|
||||||
|
@apply text-4xl font-bold text-foreground mt-8 mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 {
|
||||||
|
@apply text-3xl font-bold text-foreground mt-8 mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3 {
|
||||||
|
@apply text-2xl font-semibold text-foreground mt-6 mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p {
|
||||||
|
@apply text-base text-muted-foreground leading-relaxed mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a {
|
||||||
|
@apply text-primary hover:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul,
|
||||||
|
.prose ol {
|
||||||
|
@apply text-muted-foreground mb-6 ml-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose li {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose code {
|
||||||
|
@apply bg-muted px-2 py-1 rounded text-sm font-mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
@apply bg-muted p-4 rounded-lg overflow-x-auto mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre code {
|
||||||
|
@apply bg-transparent p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote {
|
||||||
|
@apply border-l-4 border-border pl-4 italic text-muted-foreground my-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose img {
|
||||||
|
@apply rounded-lg my-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose strong {
|
||||||
|
@apply font-semibold text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Alex Muszynski",
|
||||||
|
description: "Personal portfolio and blog",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className="dark">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
import { BlogCard } from "@/components/blog-card";
|
||||||
|
import { ProjectCard } from "@/components/project-card";
|
||||||
|
import { getBlogPosts, getProjects } from "@/lib/content";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const allPosts = getBlogPosts();
|
||||||
|
const recentPosts = allPosts.slice(0, 3);
|
||||||
|
const allProjects = getProjects();
|
||||||
|
const recentProjects = allProjects.slice(0, 3);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navigation />
|
||||||
|
<main className="container mx-auto px-6 py-16 max-w-5xl">
|
||||||
|
<section className="mb-24">
|
||||||
|
<h1 className="text-5xl font-bold text-foreground mb-6">
|
||||||
|
Hi, I'm <span className="text-primary">Alex Muszynski</span>.
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground mb-8 max-w-2xl leading-relaxed">
|
||||||
|
I'm a tinkerer that works in the realm of software. Welcome to my
|
||||||
|
portfolio where I share my projects and thoughts on tech.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="inline-flex items-center gap-2 text-lg font-medium text-foreground hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
Learn more about me
|
||||||
|
<ArrowRight size={20} />
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-24">
|
||||||
|
<div className="flex items-center justify-between mb-10">
|
||||||
|
<h2 className="text-3xl font-bold text-foreground">Recent Posts</h2>
|
||||||
|
<Link
|
||||||
|
href="/blog"
|
||||||
|
className="text-base text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
View all posts
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-10">
|
||||||
|
{recentPosts.map((post) => (
|
||||||
|
<BlogCard key={post.slug} post={post} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div className="flex items-center justify-between mb-10">
|
||||||
|
<h2 className="text-3xl font-bold text-foreground">
|
||||||
|
Recent Projects
|
||||||
|
</h2>
|
||||||
|
<Link
|
||||||
|
href="/projects"
|
||||||
|
className="text-base text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
View all projects
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{recentProjects.map((project) => (
|
||||||
|
<ProjectCard key={project.slug} project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
import { ProjectCard } from "@/components/project-card";
|
||||||
|
import { getProjects } from "@/lib/content";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Projects",
|
||||||
|
description: "A collection of my projects and side work",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
const projects = getProjects();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navigation />
|
||||||
|
<main className="container mx-auto px-6 py-16 max-w-6xl">
|
||||||
|
<div className="mb-16">
|
||||||
|
<h1 className="text-4xl font-bold text-foreground mb-4">Projects</h1>
|
||||||
|
<p className="text-lg text-muted-foreground">
|
||||||
|
A collection of projects I've built and contributed to
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<ProjectCard key={project.slug} project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { BlogPost } from '@/lib/content';
|
||||||
|
|
||||||
|
interface BlogCardProps {
|
||||||
|
post: BlogPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlogCard({ post }: BlogCardProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
className="block group"
|
||||||
|
>
|
||||||
|
<article className="space-y-3">
|
||||||
|
<time className="text-sm text-muted-foreground">
|
||||||
|
{new Date(post.date).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
<h3 className="text-xl font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-base text-muted-foreground line-clamp-2">
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export function Navigation() {
|
||||||
|
return (
|
||||||
|
<nav className="w-full border-b border-border/40">
|
||||||
|
<div className="container mx-auto px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xl font-semibold text-foreground hover:text-foreground/80 transition-colors"
|
||||||
|
>
|
||||||
|
Muszyn
|
||||||
|
</Link>
|
||||||
|
<div className="flex gap-8">
|
||||||
|
<Link
|
||||||
|
href="/blog"
|
||||||
|
className="text-base text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/projects"
|
||||||
|
className="text-base text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-base text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { Project } from '@/lib/content';
|
||||||
|
import { Github, ExternalLink, Code } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ProjectCardProps {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectCard({ project }: ProjectCardProps) {
|
||||||
|
return (
|
||||||
|
<article className="group rounded-lg border border-border/40 bg-card p-6 transition-all hover:border-border hover:shadow-lg">
|
||||||
|
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
||||||
|
{project.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-base text-muted-foreground mb-4">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 mt-4">
|
||||||
|
{project.links?.github && (
|
||||||
|
<a
|
||||||
|
href={project.links.github}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<Github size={16} />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{project.links?.gitea && (
|
||||||
|
<a
|
||||||
|
href={project.links.gitea}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<Code size={16} />
|
||||||
|
Gitea
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{project.links?.live && (
|
||||||
|
<a
|
||||||
|
href={project.links.live}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
Live Demo
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: portfolio
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.portfolio-secure.rule=Host(`site.musyzn.dev`)"
|
||||||
|
- "traefik.http.routers.portfolio-secure.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.portfolio-secure.tls=true"
|
||||||
|
- "traefik.http.routers.portfolio-secure.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.portfolio.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
- "com.dockge.project=site"
|
||||||
|
- "com.dockge.description=Personal portfolio website"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
title: 'Bluetooth on Linux'
|
||||||
|
date: '2025-12-08'
|
||||||
|
excerpt: 'Learn how to use bluetooth on a debian based linux distribution.'
|
||||||
|
---
|
||||||
|
|
||||||
|
So I've been on quite the journey learning linux as my daily driver operating system.
|
||||||
|
Today I wanted to pair my bluetooth keyboard to my desktop because.. why not, I like swapping between a few keyboards sometimes.
|
||||||
|
I went to the panel/app bar and saw no trace of a 'bluetooth' icon I had come to expect during my days on Windows and MacOS.
|
||||||
|
I attempted to look for a 'bluetooth' app to no avail and then finally went to the internet.
|
||||||
|
|
||||||
|
It didn't take long to find that my flavor of linux (mint) has a built in `bluetoothctl` tool that we can use.
|
||||||
|
|
||||||
|
## bluetoothctl
|
||||||
|
|
||||||
|
There are two ways to use `bluetoothctl` the first is by prepending every command with `bluetoothctl`. This is how the tutorial ran me through it but I wasn't a big fan and opted for the alternative way.
|
||||||
|
|
||||||
|
If you just type `bluetoothctl` you will enter an interactive utility.
|
||||||
|
|
||||||
|
```
|
||||||
|
muszyn:~/$ bluetoothctl
|
||||||
|
[Keychron K6 Pro]# Agent registered
|
||||||
|
[Keychron K6 Pro]# [CHG] Controller B4:6B:FC:6A:DB:27 Pairable: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
> You may notice that I have a bluetooth device already registered `Keychron K6 Pro`, this is the keyboard I am currently using.
|
||||||
|
|
||||||
|
Once you are in the control menu there are a list of commands to easily get started:
|
||||||
|
|
||||||
|
> you can type `help` at any time to print a list of all available commands.
|
||||||
|
|
||||||
|
- scan on | scan off
|
||||||
|
- pair `{device-mac-addr}`
|
||||||
|
- connect `{device-mac-addr}`
|
||||||
|
- devices
|
||||||
|
- remove `{device-mac-addr}`
|
||||||
|
- disconnect `{device-mac-addr}`
|
||||||
|
|
||||||
|
So lets put these commands to work.
|
||||||
|
|
||||||
|
## Scanning for Devices
|
||||||
|
|
||||||
|
To search for a bluetooth device that you can connect to use the scan command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scan on
|
||||||
|
```
|
||||||
|
|
||||||
|
This will begin discovery and output the devices that your system detects.
|
||||||
|
|
||||||
|
```
|
||||||
|
[Keychron K6 Pro]# scan on
|
||||||
|
[Keychron K6 Pro]# SetDiscoveryFilter success
|
||||||
|
[Keychron K6 Pro]# Discovery started
|
||||||
|
[Keychron K6 Pro]# [CHG] Controller B4:6B:FC:6A:DB:27 Discovering: yes
|
||||||
|
[Keychron K6 Pro]# [NEW] Device 55:41:D0:21:FD:EE 55-41-D0-21-FD-EE
|
||||||
|
[Keychron K6 Pro]# [NEW] Device 04:D5:83:26:88:0A KM18
|
||||||
|
```
|
||||||
|
|
||||||
|
The output above has a good deal of information so lets break it down.
|
||||||
|
|
||||||
|
Bluetooth devices are labeled as **Device** follwed by their Media Access Control (MAC) addresses which is a unique identifier for the device. All MAC addresses follow the format **XX:XX:XX:XX:XX:XX**. If the device has a name associated with it like *KM18*, bluetoothctl will display it. However, not every device has a display name as shown above by *55-41-D0-21-FD-EE* which is just the MAC address with '-' instead of ':'.
|
||||||
|
|
||||||
|
## Connecting to a Device
|
||||||
|
|
||||||
|
Once we have found a list of available devices we can choose to connect to one.
|
||||||
|
|
||||||
|
The first step is to pair your system to the device using the `pair` command.
|
||||||
|
Using KM18 in the list above:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pair 04:D5:83:26:88:0A
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: when using the interactive utility you can press `TAB` twice to ouput the available MAC addresses, begin typing one and then press `TAB` once to auto-fill the rest of the address.
|
||||||
|
|
||||||
|
If this isn't your first time pairing to the device then you can use the `connect` command to establish a bluetooth connection.
|
||||||
|
This is useful if your device does not automatically connect on startup or if you would like to take over the connection from another device (this happens all the time with my headphones).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
connect 04:D5:83:26:88:0A
|
||||||
|
```
|
||||||
|
|
||||||
|
## Listing Paired Devices
|
||||||
|
|
||||||
|
Using the utility listing paried devices can be useful if you don't want to `scan` all the time.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devices Paried
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output a list of your paired devices that you can then use to connect/disconnect from your system.
|
||||||
|
|
||||||
|
## Disconnecting Devices
|
||||||
|
|
||||||
|
To unpair the device use the `remove` command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remove 04:D5:83:26:88:0A
|
||||||
|
```
|
||||||
|
|
||||||
|
To disconnect the device use the `disconnect` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
disconnect 04:D5:83:26:88:0A
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exiting the Interactive Mode
|
||||||
|
|
||||||
|
To exit, either type `exit`, or `quit`, OR you can use `ctl+D` whichever you prefer.
|
||||||
|
|
||||||
|
## Non-Interactive
|
||||||
|
|
||||||
|
If you want to use the non-interactive tool then simply place `bluetoothctl` in front of each command.
|
||||||
|
The primary benefit I found for the interactive tool was the tab completion for the MAC addresses :D.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: 'Homelab Server'
|
||||||
|
description: 'A personal server that replicates a plethora of self-hosted solutions'
|
||||||
|
links:
|
||||||
|
github: 'https://github.com/amuszyn/homelab'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Homelab Server
|
||||||
|
|
||||||
|
A personal server that replicates a plethora of self-hosted solutions.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Git server
|
||||||
|
- Reverse proxy
|
||||||
|
- Log aggregation and filtering
|
||||||
|
- Monitoring and visualization
|
||||||
|
- Network wide ad-blocking
|
||||||
|
- VPN
|
||||||
|
- Automatic photo backup
|
||||||
|
- Network Security
|
||||||
|
|
||||||
|
## Built With
|
||||||
|
|
||||||
|
- Dockge
|
||||||
|
- Docker
|
||||||
|
- Graphana
|
||||||
|
- Prometheus
|
||||||
|
- Traefik
|
||||||
|
- Gitea
|
||||||
|
- Wireguard
|
||||||
|
- PiHole
|
||||||
|
- Cloudflare
|
||||||
|
- Immich
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: 'Portfolio Website'
|
||||||
|
description: 'A modern portfolio built with Next.js, TypeScript, and shadcn/ui'
|
||||||
|
links:
|
||||||
|
gitea: 'https://gitea.muszyn.dev/muszyn/portfolio'
|
||||||
|
live: 'https://site.muszyn.dev'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Portfolio Website
|
||||||
|
|
||||||
|
A fully responsive portfolio website built with the latest web technologies. Features a clean, minimalist design with dark mode support.
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
- Next.js 16
|
||||||
|
- TypeScript
|
||||||
|
- shadcn/ui
|
||||||
|
- Tailwind CSS
|
||||||
|
- Markdown for content management
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Blog with markdown support
|
||||||
|
- Project showcase
|
||||||
|
- Fully responsive design
|
||||||
|
- Dark mode
|
||||||
|
- Fast static site generation
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
|
||||||
|
const contentDirectory = path.join(process.cwd(), 'content');
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
excerpt: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
links?: {
|
||||||
|
github?: string;
|
||||||
|
live?: string;
|
||||||
|
gitea?: string;
|
||||||
|
};
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlogPosts(): BlogPost[] {
|
||||||
|
const blogDirectory = path.join(contentDirectory, 'blog');
|
||||||
|
|
||||||
|
if (!fs.existsSync(blogDirectory)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNames = fs.readdirSync(blogDirectory);
|
||||||
|
const allPosts = fileNames
|
||||||
|
.filter((fileName) => fileName.endsWith('.md'))
|
||||||
|
.map((fileName) => {
|
||||||
|
const slug = fileName.replace(/\.md$/, '');
|
||||||
|
const fullPath = path.join(blogDirectory, fileName);
|
||||||
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContents);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title: data.title || '',
|
||||||
|
date: data.date || '',
|
||||||
|
excerpt: data.excerpt || '',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return allPosts.sort((a, b) => (a.date > b.date ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlogPost(slug: string): BlogPost | null {
|
||||||
|
const blogDirectory = path.join(contentDirectory, 'blog');
|
||||||
|
const fullPath = path.join(blogDirectory, `${slug}.md`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContents);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title: data.title || '',
|
||||||
|
date: data.date || '',
|
||||||
|
excerpt: data.excerpt || '',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjects(): Project[] {
|
||||||
|
const projectsDirectory = path.join(contentDirectory, 'projects');
|
||||||
|
|
||||||
|
if (!fs.existsSync(projectsDirectory)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNames = fs.readdirSync(projectsDirectory);
|
||||||
|
const allProjects = fileNames
|
||||||
|
.filter((fileName) => fileName.endsWith('.md'))
|
||||||
|
.map((fileName) => {
|
||||||
|
const slug = fileName.replace(/\.md$/, '');
|
||||||
|
const fullPath = path.join(projectsDirectory, fileName);
|
||||||
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContents);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title: data.title || '',
|
||||||
|
description: data.description || '',
|
||||||
|
links: data.links,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return allProjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProject(slug: string): Project | null {
|
||||||
|
const projectsDirectory = path.join(contentDirectory, 'projects');
|
||||||
|
const fullPath = path.join(projectsDirectory, `${slug}.md`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContents);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title: data.title || '',
|
||||||
|
description: data.description || '',
|
||||||
|
links: data.links,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
reactCompiler: true,
|
||||||
|
output: 'standalone',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-variance-authority": "0.7.1",
|
||||||
|
"clsx": "2.1.1",
|
||||||
|
"gray-matter": "4.0.3",
|
||||||
|
"lucide-react": "0.561.0",
|
||||||
|
"next": "16.0.10",
|
||||||
|
"react": "19.2.1",
|
||||||
|
"react-dom": "19.2.1",
|
||||||
|
"react-markdown": "10.1.0",
|
||||||
|
"remark-gfm": "4.0.1",
|
||||||
|
"tailwind-merge": "3.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
|
"@types/node": "20.19.26",
|
||||||
|
"@types/react": "19.2.7",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
|
"eslint": "9.39.1",
|
||||||
|
"eslint-config-next": "16.0.10",
|
||||||
|
"tailwindcss": "4.1.18",
|
||||||
|
"tw-animate-css": "1.4.0",
|
||||||
|
"typescript": "5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"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": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||