Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
![Test Coverage](https://img.shields.io/codecov/c/github/hypkey/devpockit)
![Version](https://img.shields.io/github/package-json/v/hypkey/devpockit)

A modern web application providing essential developer tools with a clean, responsive interface. Built with Next.js 15, featuring 30+ powerful tools that run entirely client-side for optimal performance and privacy.
A modern web application providing essential developer tools with a clean, responsive interface. Built with Next.js (App Router, static export), featuring 30+ powerful tools that run entirely client-side for optimal performance and privacy.

🌐 **[Production](https://devpockit.hypkey.com/)** | πŸ“– **[Documentation](#-documentation)** | 🀝 **[Contributing](CONTRIBUTING.md)** | πŸ“ **[Changelog](CHANGELOG.md)**

Expand All @@ -14,7 +14,7 @@ A modern web application providing essential developer tools with a clean, respo
- **30+ Developer Tools** - JSON formatter, UUID generator, JWT decoder, regex tester, and more
- **Client-Side Processing** - All tools run in your browser, no data sent to servers
- **Modern UI** - Clean, responsive design with dark/light theme support
- **Fast & Reliable** - Built with Next.js 15 and TypeScript
- **Fast & Reliable** - Built with Next.js and TypeScript
- **Mobile Friendly** - Works seamlessly on desktop, tablet, and mobile devices
- **Open Source** - MIT licensed, free to use and contribute

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "devpockit-frontend",
"version": "0.2.0",
"version": "0.2.1",
"description": "DevPockit Frontend - Developer Tools Web App",
"license": "MIT",
"repository": {
Expand Down
17 changes: 17 additions & 0 deletions public/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "DevPockit",
"short_name": "DevPockit",
"description": "Free developer tools that run in your browser.",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ea580c",
"icons": [
{
"src": "/assets/devpockit-logo.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
22 changes: 17 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AppLayout } from '@/components/layout/AppLayout'
import { AppToastProvider } from '@/components/providers/AppToastProvider'
import { ThemeProvider } from '@/components/providers/ThemeProvider'
import { absoluteAssetUrl, absoluteSiteUrl } from '@/libs/site-url'
import type { Metadata, Viewport } from 'next'
import { DM_Serif_Text, Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
Expand All @@ -23,8 +25,12 @@ const dmSerifText = DM_Serif_Text({

const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://devpockit.hypkey.com';

const canonicalHome = absoluteSiteUrl('/')
const ogImageUrl = absoluteAssetUrl('/og-image.png')

export const metadata: Metadata = {
metadataBase: new URL(baseUrl),
manifest: absoluteAssetUrl('/manifest.webmanifest'),
title: {
default: 'DevPockit - Free Online Developer Tools',
template: '%s | DevPockit',
Expand Down Expand Up @@ -78,14 +84,14 @@ export const metadata: Metadata = {
openGraph: {
type: 'website',
locale: 'en_US',
url: `${baseUrl}/`,
url: canonicalHome,
siteName: 'DevPockit',
title: 'DevPockit - Free Online Developer Tools',
description:
'Free online developer tools that run locally in your browser. JSON formatter, UUID generator, JWT decoder, and 25+ more tools. Fast, private, no sign-up.',
images: [
{
url: '/og-image.png',
url: ogImageUrl,
width: 1200,
height: 630,
alt: 'DevPockit - Developer Tools',
Expand All @@ -97,17 +103,21 @@ export const metadata: Metadata = {
title: 'DevPockit - Free Online Developer Tools',
description:
'Free developer tools in your browser. JSON formatter, UUID generator, JWT decoder & more. Private, fast, no sign-up.',
images: ['/og-image.png'],
images: [ogImageUrl],
},
alternates: {
canonical: `${baseUrl}/`,
canonical: canonicalHome,
},
category: 'technology',
}

export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },
],
}

export default function RootLayout({
Expand All @@ -124,7 +134,9 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<AppLayout>{children}</AppLayout>
<AppToastProvider>
<AppLayout>{children}</AppLayout>
</AppToastProvider>
</ThemeProvider>
</body>
</html>
Expand Down
35 changes: 35 additions & 0 deletions src/app/tools/[category]/[toolId]/[instanceId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getToolById, getTools } from '@/libs/tools-data';
import { absoluteAssetUrl, absoluteSiteUrl } from '@/libs/site-url';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface ToolPageProps {
Expand Down Expand Up @@ -34,6 +36,39 @@ export async function generateStaticParams() {
return params;
}

export async function generateMetadata({ params }: ToolPageProps): Promise<Metadata> {
const { category, toolId, instanceId } = await params;
const tool = getToolById(toolId);

if (!tool || tool.category !== category) {
return { title: 'Tool' };
}

const title = `${tool.name} - Free Online Tool`;
const description = `${tool.description} Free, fast, and runs locally in your browser. No sign-up required.`;
const canonicalPath = `/tools/${category}/${toolId}/${instanceId}/`;
const canonical = absoluteSiteUrl(canonicalPath);
const ogImage = absoluteAssetUrl('/og-image.png');

return {
title,
description,
openGraph: {
title: `${tool.name} | DevPockit`,
description,
url: canonical,
type: 'website',
images: [{ url: ogImage, width: 1200, height: 630, alt: tool.name }],
},
twitter: {
card: 'summary_large_image',
title: `${tool.name} | DevPockit`,
description,
},
alternates: { canonical },
};
}

export default async function ToolPage({ params }: ToolPageProps) {
try {
const { category, toolId, instanceId } = await params;
Expand Down
11 changes: 6 additions & 5 deletions src/app/tools/[category]/[toolId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getCategoryById, getToolById, getTools } from '@/libs/tools-data';
import { absoluteAssetUrl, absoluteSiteUrl } from '@/libs/site-url';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';

Expand Down Expand Up @@ -51,9 +52,9 @@ export async function generateMetadata({ params }: ToolPageProps): Promise<Metad
const title = `${tool.name} - Free Online Tool`;
const description = `${tool.description} Free, fast, and runs locally in your browser. No sign-up required.`;

// Ensure trailing slash for GitHub Pages compatibility
const toolUrl = tool.path.endsWith('/') ? tool.path : `${tool.path}/`;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://devpockit.hypkey.com';
const canonical = absoluteSiteUrl(toolUrl);
const ogImage = absoluteAssetUrl('/og-image.png');

return {
title,
Expand All @@ -62,11 +63,11 @@ export async function generateMetadata({ params }: ToolPageProps): Promise<Metad
openGraph: {
title: `${tool.name} | DevPockit`,
description,
url: `${baseUrl}${toolUrl}`,
url: canonical,
type: 'website',
images: [
{
url: '/og-image.png',
url: ogImage,
width: 1200,
height: 630,
alt: tool.name,
Expand All @@ -79,7 +80,7 @@ export async function generateMetadata({ params }: ToolPageProps): Promise<Metad
description,
},
alternates: {
canonical: `${baseUrl}${toolUrl}`,
canonical,
},
};
}
Expand Down
26 changes: 26 additions & 0 deletions src/app/tools/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getCategories, getCategoryById } from '@/libs/tools-data';
import { absoluteSiteUrl } from '@/libs/site-url';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface CategoryPageProps {
Expand All @@ -20,6 +22,30 @@ export async function generateStaticParams() {
}));
}

export async function generateMetadata({ params }: CategoryPageProps): Promise<Metadata> {
const { category: categoryId } = await params;
const category = getCategoryById(categoryId);
if (!category) {
return { title: 'Tools' };
}

const title = `${category.name} β€” Developer Tools`;
const description = `${category.description}. Free browser-based tools on DevPockit.`;
const canonical = absoluteSiteUrl(`/tools/${category.id}/`);

return {
title,
description,
openGraph: {
title: `${category.name} | DevPockit`,
description,
url: canonical,
type: 'website',
},
alternates: { canonical },
};
}

export default async function CategoryPage({ params }: CategoryPageProps) {
try {
const { category: categoryId } = await params;
Expand Down
87 changes: 74 additions & 13 deletions src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { SearchTools } from "@/components/layout/SearchTools"
import { useToolActivity } from "@/components/providers/ToolActivityProvider"
import {
Collapsible,
CollapsibleContent,
Expand Down Expand Up @@ -49,6 +50,7 @@ import {
RefreshCw,
Search,
Settings,
Star,
Sun,
type LucideIcon,
} from "lucide-react"
Expand Down Expand Up @@ -95,6 +97,7 @@ export function AppSidebar({
const pathname = usePathname()
const { theme, setTheme } = useTheme()
const { state, toggleSidebar } = useSidebar()
const { hydrated: pinsHydrated, isPinned, togglePinnedTool } = useToolActivity()
const [codeEditorTheme, setCodeEditorTheme] = useCodeEditorTheme('basicDark')
const [mounted, setMounted] = React.useState(false)
const [openCategories, setOpenCategories] = React.useState<Set<string>>(new Set())
Expand Down Expand Up @@ -292,18 +295,46 @@ export function AppSidebar({
{category.tools.map((tool) => (
<div
key={tool.id}
className="text-xs cursor-pointer hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-700 dark:hover:text-neutral-100 dark:text-neutral-100 py-1 px-2 rounded-md -mx-2"
onClick={(e) => {
e.stopPropagation()
handleToolSelect(tool.id)
}}
className="flex items-center gap-1 -mx-2 rounded-md px-2 py-1 text-xs"
>
{tool.name}
<div
className="min-w-0 flex-1 cursor-pointer rounded-md py-1 px-2 -mx-1 hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-700 dark:hover:text-neutral-100"
onClick={(e) => {
e.stopPropagation()
handleToolSelect(tool.id)
}}
>
<span className="block truncate">{tool.name}</span>
</div>
{mounted && pinsHydrated ? (
<button
type="button"
className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-neutral-500 hover:bg-neutral-200 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-600 dark:hover:text-neutral-100',
isPinned(tool.id) && 'text-orange-600 dark:text-orange-400'
)}
aria-label={isPinned(tool.id) ? `Unpin ${tool.name}` : `Pin ${tool.name}`}
aria-pressed={isPinned(tool.id)}
onClick={(e) => {
e.stopPropagation()
togglePinnedTool(tool.id)
}}
>
<Star
className={cn(
'h-3.5 w-3.5',
isPinned(tool.id) &&
'fill-orange-500 text-orange-600 dark:fill-orange-500 dark:text-orange-400'
)}
strokeWidth={1.5}
/>
</button>
) : null}
</div>
))}
</div>
),
className: "w-48",
className: "w-56",
}}
>
<IconComponent className="h-4 w-4" />
Expand All @@ -315,12 +346,42 @@ export function AppSidebar({
<SidebarMenuSub>
{category.tools.map((tool) => (
<SidebarMenuSubItem key={tool.id}>
<SidebarMenuSubButton
isActive={tool.id === selectedTool}
onClick={() => handleToolSelect(tool.id)}
>
<span>{tool.name}</span>
</SidebarMenuSubButton>
<div className="flex w-full min-w-0 items-center gap-0.5">
<SidebarMenuSubButton
className="min-w-0 flex-1 pr-0"
isActive={tool.id === selectedTool}
onClick={() => handleToolSelect(tool.id)}
>
<span>{tool.name}</span>
</SidebarMenuSubButton>
{pinsHydrated ? (
<button
type="button"
className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
isPinned(tool.id) && 'text-orange-600 dark:text-orange-400'
)}
aria-label={
isPinned(tool.id) ? `Unpin ${tool.name}` : `Pin ${tool.name}`
}
aria-pressed={isPinned(tool.id)}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
togglePinnedTool(tool.id)
}}
>
<Star
className={cn(
'h-3.5 w-3.5',
isPinned(tool.id) &&
'fill-orange-500 text-orange-600 dark:fill-orange-500 dark:text-orange-400'
)}
strokeWidth={1.5}
/>
</button>
) : null}
</div>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
Expand Down
Loading
Loading