Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: npm ci --ignore-scripts

- name: Audit dependencies (high+critical)
run: npm audit --audit-level=high
run: npm audit --audit-level=high --omit=optional

- name: Typecheck
run: npx tsc --noEmit
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: npm ci --ignore-scripts

- name: Audit — fail on high/critical CVEs
run: npm audit --audit-level=high
run: npm audit --audit-level=high --omit=optional

- name: Typecheck
run: npx tsc --noEmit
Expand Down
14 changes: 14 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const metadata: Metadata = {
follow: true,
googleBot: { index: true, follow: true, 'max-image-preview': 'large' },
},
manifest: '/manifest.json',
alternates: {
canonical: BASE_URL,
},
Expand All @@ -42,6 +43,19 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<head>
<script
dangerouslySetInnerHTML={{
__html: `
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').catch(function() {});
});
}
`,
}}
/>
</head>
<body className="antialiased">{children}</body>
</html>
)
Expand Down
23 changes: 23 additions & 0 deletions app/tools/[toolId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ export default async function ToolPage({ params }: Props) {
{/* Tool Content */}
<ToolRenderer toolId={toolId} />

{/* Programmatic Educational SEO & Security Guarantee Section */}
<section className="mt-12 pt-8 border-t border-zinc-800/80 space-y-4">
<h2 className="text-lg font-semibold text-zinc-200">About {tool.name}</h2>
<p className="text-xs text-zinc-400 leading-relaxed">
The <strong>{tool.name}</strong> tool allows developers, security auditors, and engineers to perform {tool.description.toLowerCase()} in a secure, instant, and private manner.
</p>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<div className="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 space-y-2">
<h3 className="text-xs font-semibold text-emerald-400 uppercase tracking-wider">🔒 100% Client-Side Privacy</h3>
<p className="text-xs text-zinc-400 leading-relaxed">
All computations, key generations, hashing, and conversions are calculated locally inside your web browser. No plain text, secret keys, or uploaded files ever leave your device or reach an external server.
</p>
</div>
<div className="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 space-y-2">
<h3 className="text-xs font-semibold text-blue-400 uppercase tracking-wider">⚡ Offline & PWA Ready</h3>
<p className="text-xs text-zinc-400 leading-relaxed">
DevCipher is fully usable offline and in air-gapped security environments. Install it as a Desktop Progressive Web App (PWA) via your browser's install menu.
</p>
</div>
</div>
</section>

{/* Related Tools */}
{related.length > 0 && (
<div className="mt-10 pt-8 border-t border-zinc-800">
Expand Down
9 changes: 8 additions & 1 deletion components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React, { useState } from 'react'
import Sidebar from './Sidebar'
import Header from './Header'
import CommandPalette from './CommandPalette'

interface AppShellProps {
children: React.ReactNode
Expand All @@ -10,12 +11,18 @@ interface AppShellProps {

export default function AppShell({ children, title }: AppShellProps) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const [paletteOpen, setPaletteOpen] = useState(false)

return (
<div className="min-h-screen bg-zinc-950 text-zinc-100">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<CommandPalette isOpen={paletteOpen} onClose={() => setPaletteOpen(false)} />
<div className="lg:pl-64 flex flex-col min-h-screen">
<Header onMenuClick={() => setSidebarOpen(true)} title={title} />
<Header
onMenuClick={() => setSidebarOpen(true)}
onSearchClick={() => setPaletteOpen(true)}
title={title}
/>
<main className="flex-1 p-4 md:p-6 max-w-5xl w-full mx-auto">
{children}
</main>
Expand Down
166 changes: 166 additions & 0 deletions components/layout/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { ALL_TOOLS, findCategory } from '@/lib/tools-config'
import { Search, X, Command, ArrowRight, CornerDownLeft } from 'lucide-react'

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import ArrowRight.

interface CommandPaletteProps {
isOpen: boolean
onClose: () => void
}

export default function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
const router = useRouter()
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0)

const filteredTools = ALL_TOOLS.filter((t) => {
if (!query.trim()) return true
const q = query.toLowerCase()
return (
t.name.toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q) ||
t.category.toLowerCase().includes(q) ||
(t.tags && t.tags.some((tag) => tag.toLowerCase().includes(q))) ||
(t.keywords && t.keywords.toLowerCase().includes(q))
)
}).slice(0, 10)

const handleSelect = useCallback(
(toolId: string) => {
onClose()
setQuery('')
router.push(`/tools/${toolId}`)
},
[onClose, router]
)

useEffect(() => {
setSelectedIndex(0)
}, [query])

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault()
if (isOpen) onClose()
else setQuery('')
}
if (!isOpen) return

if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedIndex((prev) => (prev + 1) % Math.max(1, filteredTools.length))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedIndex((prev) => (prev - 1 + filteredTools.length) % Math.max(1, filteredTools.length))
} else if (e.key === 'Enter' && filteredTools[selectedIndex]) {
e.preventDefault()
handleSelect(filteredTools[selectedIndex].id)
} else if (e.key === 'Escape') {
e.preventDefault()
onClose()
}
}

window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose, filteredTools, selectedIndex, handleSelect])

if (!isOpen) return null

return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-150">
<div
className="fixed inset-0"
onClick={onClose}
aria-hidden="true"
/>
<div className="relative w-full max-w-xl rounded-xl border border-zinc-800 bg-zinc-950 shadow-2xl overflow-hidden z-10 flex flex-col max-h-[80vh]">
{/* Search Bar Header */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-zinc-800 bg-zinc-900/60">
<Search className="h-4 w-4 text-blue-400 flex-shrink-0" />
<input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search tools... (e.g. SHA256, JWT, RSA, PQC, Base64)"
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none"
/>
{query && (
<button
onClick={() => setQuery('')}
className="text-zinc-500 hover:text-zinc-300 p-0.5 rounded"
>
<X className="h-4 w-4" />
</button>
)}
<button
onClick={onClose}
className="text-xs text-zinc-500 hover:text-zinc-300 bg-zinc-800/80 px-2 py-1 rounded border border-zinc-700/50"
>
Esc
</button>
</div>

{/* Results List */}
<div className="overflow-y-auto p-2 space-y-1 divide-y divide-zinc-900">
{filteredTools.length > 0 ? (
filteredTools.map((tool, idx) => {
const cat = findCategory(tool.category)
const isSelected = idx === selectedIndex
return (
<button
key={tool.id}
onClick={() => handleSelect(tool.id)}
onMouseEnter={() => setSelectedIndex(idx)}
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-left text-xs transition-all ${
isSelected
? 'bg-blue-600/20 text-white border border-blue-800/60'
: 'text-zinc-300 hover:bg-zinc-900 border border-transparent'
}`}
>
<div className="flex items-center gap-3 min-w-0 pr-2">
<span
className={`text-[10px] uppercase font-semibold tracking-wider px-2 py-0.5 rounded border ${
cat?.color ?? 'text-zinc-400'
} bg-zinc-900/80 border-zinc-800`}
>
{cat?.label ?? tool.category}
</span>
<div className="min-w-0">
<p className="font-medium text-sm text-zinc-100 truncate">{tool.name}</p>
<p className="text-[11px] text-zinc-400 truncate">{tool.description}</p>
</div>
</div>
<div className="flex items-center gap-1.5 text-zinc-500 flex-shrink-0">
{isSelected && (
<span className="flex items-center gap-1 text-[10px] text-blue-400 bg-blue-950/60 px-1.5 py-0.5 rounded border border-blue-800/40">
Open <CornerDownLeft className="h-3 w-3" />
</span>
)}
</div>
</button>
)
})
) : (
<div className="py-8 text-center text-xs text-zinc-500">
No matching tools found for "{query}"
</div>
)}
</div>

{/* Footer Hint */}
<div className="px-4 py-2 border-t border-zinc-800/80 bg-zinc-900/40 flex items-center justify-between text-[11px] text-zinc-500">
<span className="flex items-center gap-1.5">
<Command className="h-3 w-3 text-zinc-400" /> + <kbd className="font-mono">K</kbd> to toggle anywhere
</span>
<span className="flex items-center gap-2">
<span>Use ↑ ↓ to navigate</span>
<span>↵ to select</span>
</span>
</div>
</div>
</div>
)
}
24 changes: 20 additions & 4 deletions components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client'
import { Menu, ExternalLink, Code2 } from 'lucide-react'
import { Menu, ExternalLink, Code2, Search, Command } from 'lucide-react'

interface HeaderProps {
onMenuClick: () => void
onSearchClick?: () => void
title?: string
}

export default function Header({ onMenuClick, title }: HeaderProps) {
export default function Header({ onMenuClick, onSearchClick, title }: HeaderProps) {
return (
<header className="sticky top-0 z-10 flex h-13 items-center gap-3 border-b border-zinc-800/80 bg-zinc-950/80 backdrop-blur-sm px-4">
<button
Expand All @@ -15,16 +16,31 @@ export default function Header({ onMenuClick, title }: HeaderProps) {
>
<Menu className="h-5 w-5" />
</button>
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0 flex items-center gap-4">
{title && (
<h1 className="text-sm font-semibold text-zinc-100 truncate">{title}</h1>
)}
</div>

{/* Cmd + K Button */}
{onSearchClick && (
<button
onClick={onSearchClick}
className="flex items-center gap-2 rounded-lg bg-zinc-900 border border-zinc-800 hover:border-zinc-700 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-200 transition-all shadow-sm"
>
<Search className="h-3.5 w-3.5 text-blue-400" />
<span className="hidden sm:inline">Search tools...</span>
<span className="flex items-center gap-0.5 rounded bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500 font-mono border border-zinc-700/50">
<Command className="h-2.5 w-2.5" />K
</span>
</button>
)}

<a
href="https://github.com/AnimeshShaw/DevCipher"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-zinc-300 transition-colors ml-1"
>
<Code2 className="h-4 w-4" />
<span className="hidden sm:inline">GitHub</span>
Expand Down
Loading
Loading