Skip to content
Draft
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
41 changes: 41 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
18 changes: 18 additions & 0 deletions app/components/CategoryBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Category } from '@/types';

const colorMap: Record<Category, string> = {
算法: 'bg-blue-500/20 text-blue-300 border border-blue-500/30',
系统设计: 'bg-purple-500/20 text-purple-300 border border-purple-500/30',
行为面试: 'bg-green-500/20 text-green-300 border border-green-500/30',
数据库: 'bg-orange-500/20 text-orange-300 border border-orange-500/30',
网络: 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30',
操作系统: 'bg-red-500/20 text-red-300 border border-red-500/30',
};

export default function CategoryBadge({ category }: { category: Category }) {
return (
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${colorMap[category]}`}>
{category}
</span>
);
}
21 changes: 21 additions & 0 deletions app/components/DifficultyBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Difficulty } from '@/types';

const colorMap: Record<Difficulty, string> = {
Easy: 'bg-green-500/20 text-green-300 border border-green-500/30',
Medium: 'bg-amber-500/20 text-amber-300 border border-amber-500/30',
Hard: 'bg-red-500/20 text-red-300 border border-red-500/30',
};

const labelMap: Record<Difficulty, string> = {
Easy: '简单',
Medium: '中等',
Hard: '困难',
};

export default function DifficultyBadge({ difficulty }: { difficulty: Difficulty }) {
return (
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${colorMap[difficulty]}`}>
{labelMap[difficulty]}
</span>
);
}
99 changes: 99 additions & 0 deletions app/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';

const navLinks = [
{ href: '/', label: '首页' },
{ href: '/questions', label: '题库' },
{ href: '/interview', label: '模拟面试' },
{ href: '/dashboard', label: '仪表盘' },
];

export default function Navigation() {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);

return (
<nav className="bg-gradient-to-r from-blue-900 via-blue-800 to-purple-900 shadow-lg shadow-blue-900/50 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="flex items-center gap-2 group">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center shadow-md group-hover:scale-110 transition-transform">
<span className="text-xl">🧠</span>
</div>
<span className="text-white font-bold text-lg tracking-tight hidden sm:block">
智能面试平台
</span>
</Link>

{/* Desktop links */}
<div className="hidden md:flex items-center gap-1">
{navLinks.map((link) => {
const isActive =
link.href === '/' ? pathname === '/' : pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
isActive
? 'bg-white/20 text-white shadow-inner'
: 'text-blue-100 hover:bg-white/10 hover:text-white'
}`}
>
{link.label}
</Link>
);
})}
</div>

{/* Mobile hamburger */}
<button
className="md:hidden text-white p-2 rounded-lg hover:bg-white/10 transition-colors"
onClick={() => setMobileOpen((v) => !v)}
aria-label="切换菜单"
>
{mobileOpen ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>

{/* Mobile menu */}
{mobileOpen && (
<div className="md:hidden border-t border-white/10 bg-blue-900/95 backdrop-blur-sm">
<div className="px-4 py-3 flex flex-col gap-1">
{navLinks.map((link) => {
const isActive =
link.href === '/' ? pathname === '/' : pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className={`px-4 py-3 rounded-lg text-sm font-medium transition-all ${
isActive
? 'bg-white/20 text-white'
: 'text-blue-100 hover:bg-white/10 hover:text-white'
}`}
>
{link.label}
</Link>
);
})}
</div>
</div>
)}
</nav>
);
}
54 changes: 54 additions & 0 deletions app/components/QuestionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Link from 'next/link';
import { Question } from '@/types';
import CategoryBadge from './CategoryBadge';
import DifficultyBadge from './DifficultyBadge';

export default function QuestionCard({ question }: { question: Question }) {
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5 hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 hover:scale-[1.02] transition-all duration-300 flex flex-col gap-3">
{/* Badges */}
<div className="flex items-center gap-2 flex-wrap">
<CategoryBadge category={question.category} />
<DifficultyBadge difficulty={question.difficulty} />
</div>

{/* Title */}
<h3 className="text-white font-semibold text-base leading-snug line-clamp-2">
{question.title}
</h3>

{/* Description preview */}
<p className="text-gray-400 text-sm leading-relaxed line-clamp-2 flex-1">
{question.description}
</p>

{/* Tags */}
<div className="flex flex-wrap gap-1.5">
{question.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="text-xs bg-gray-800 text-gray-400 px-2 py-0.5 rounded-md border border-gray-700"
>
{tag}
</span>
))}
</div>

{/* Footer */}
<div className="flex items-center justify-between pt-1 border-t border-gray-800">
<span className="text-xs text-gray-500">
限时 {Math.floor(question.timeLimit / 60)} 分钟
</span>
<Link
href={`/questions/${question.id}`}
className="text-sm font-medium text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-1"
>
查看详情
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
</div>
</div>
);
}
Loading