diff --git a/Frontend/.eslintrc.json b/Frontend/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/Frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/Frontend/.gitignore b/Frontend/.gitignore new file mode 100644 index 0000000..9b1913e --- /dev/null +++ b/Frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# 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 +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Frontend/Dockerfile b/Frontend/Dockerfile new file mode 100644 index 0000000..7c544db --- /dev/null +++ b/Frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:21-alpine as stage1 +WORKDIR /app +COPY package.json ./ +RUN npm install + +FROM node:21-alpine as stage2 +WORKDIR /app +COPY . . +COPY --from=stage1 /app/node_modules ./node_modules +RUN npm run build + +FROM node:21-alpine as final +WORKDIR /app +ENV NODE_ENV production +COPY --from=stage2 /app ./ + +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/Frontend/app/dashboard/page.jsx b/Frontend/app/dashboard/page.jsx new file mode 100644 index 0000000..5b947c1 --- /dev/null +++ b/Frontend/app/dashboard/page.jsx @@ -0,0 +1,131 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { motion } from 'framer-motion'; +import { Star, Zap, Trophy, ArrowRight, LogOut } from 'lucide-react'; +import useStore from '@/lib/store'; +import MobileNav from '@/components/navigation/mobile-nav'; +import CosmicCard from '@/components/ui/cosmic-card'; +import ProgressBar from '@/components/ui/progress-bar'; +import StatCard from '@/components/ui/stat-card'; + +export default function DashboardPage() { + const router = useRouter(); + const { user, missions, setUser } = useStore(); + + useEffect(() => { + if (!user) { + router.push('/login'); + } + }, [user, router]); + + if (!user) return null; + + const availableMissions = missions.filter((m) => m.status === 'available').length; + const completedMissions = missions.filter((m) => m.status === 'completed').length; + + const handleLogout = () => { + setUser(null); + router.push('/login'); + }; + + return ( +
+
+ +
+
+
+

Главная

+

Добро пожаловать, {user.name}

+
+ +
+ + +
+
+ +
+
+

{user.rank}

+

Следующий ранг: {user.nextRank}

+
+
+ + + +
+ + {user.experience} / {user.neededExperience} XP + +
+
+ +
+ + +
+ + +

Навыки

+
+ {user.skills.map((skill) => ( + +
+ {skill.name} + Уровень {skill.level} +
+ +
+ ))} +
+
+ + +
+

Статистика

+
+
+
+

Доступно миссий

+

{availableMissions}

+
+
+

Завершено миссий

+

{completedMissions}

+
+
+
+ + router.push('/missions')} + className="w-full p-4 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg font-semibold text-white flex items-center justify-center gap-2 hover:from-blue-600 hover:to-purple-700 transition-all" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Перейти к миссиям + + +
+ + +
+ ); +} \ No newline at end of file diff --git a/Frontend/app/globals.css b/Frontend/app/globals.css new file mode 100644 index 0000000..972259c --- /dev/null +++ b/Frontend/app/globals.css @@ -0,0 +1,92 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #0a0e27 0%, #1a0b2e 50%, #16213e 100%); + min-height: 100vh; +} + +.cosmic-bg { + background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.1) 0%, transparent 50%), + radial-gradient(circle at 20% 80%, rgba(236, 72, 153, 0.1) 0%, transparent 50%); +} + +.stars-bg { + background-image: + radial-gradient(2px 2px at 20px 30px, white, transparent), + radial-gradient(2px 2px at 60px 70px, white, transparent), + radial-gradient(1px 1px at 50px 50px, white, transparent), + radial-gradient(1px 1px at 130px 80px, white, transparent), + radial-gradient(2px 2px at 90px 10px, white, transparent); + background-size: 200px 200px; + background-repeat: repeat; + opacity: 0.3; +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/Frontend/app/hr/dashboard/page.jsx b/Frontend/app/hr/dashboard/page.jsx new file mode 100644 index 0000000..71d79ed --- /dev/null +++ b/Frontend/app/hr/dashboard/page.jsx @@ -0,0 +1,167 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { motion } from 'framer-motion'; +import { Target, Users, CircleCheck as CheckCircle, Clock, TrendingUp, Star } from 'lucide-react'; +import useStore from '@/lib/store'; +import HRSidebar from '@/components/navigation/hr-sidebar'; +import CosmicCard from '@/components/ui/cosmic-card'; +import StatCard from '@/components/ui/stat-card'; + +export default function HRDashboardPage() { + const router = useRouter(); + const { user, missions, isHR } = useStore(); + + useEffect(() => { + if (!user) { + router.push('/login'); + } else if (!isHR) { + router.push('/dashboard'); + } + }, [user, isHR, router]); + + if (!user || !isHR) return null; + + const totalMissions = missions.length; + const completedMissions = missions.filter((m) => m.status === 'completed').length; + const availableMissions = missions.filter((m) => m.status === 'available').length; + const completionRate = totalMissions > 0 ? Math.round((completedMissions / totalMissions) * 100) : 0; + + const missionsByCategory = missions.reduce((acc, mission) => { + acc[mission.category] = (acc[mission.category] || 0) + 1; + return acc; + }, {}); + + return ( +
+ + +
+
+ +
+
+

HR Дашборд

+

Управление системой геймификации

+
+ +
+ + + + +
+ +
+ +

+ + Миссии по категориям +

+
+ {Object.entries(missionsByCategory).map(([category, count], index) => ( + +
+ {category} + {count} +
+
+ ))} +
+
+ + +

+ + Быстрые действия +

+
+ router.push('/hr/missions/create')} + className="w-full p-4 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg font-semibold text-white hover:from-blue-600 hover:to-purple-700 transition-all text-left" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Создать новую миссию + + + router.push('/hr/missions')} + className="w-full p-4 bg-slate-800 rounded-lg font-semibold text-white hover:bg-slate-700 transition-all text-left border border-slate-700" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Управление миссиями + + + router.push('/hr/users')} + className="w-full p-4 bg-slate-800 rounded-lg font-semibold text-white hover:bg-slate-700 transition-all text-left border border-slate-700" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Просмотр пользователей + +
+
+
+ + +

+ + Последние завершенные миссии +

+
+ {missions + .filter((m) => m.status === 'completed') + .slice(0, 5) + .map((mission, index) => ( + +
+
+

{mission.title}

+

{mission.category}

+
+
+
+ + +{mission.experienceReward} +
+
+
+
+ ))} + {missions.filter((m) => m.status === 'completed').length === 0 && ( +

+ Пока нет завершенных миссий +

+ )} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/Frontend/app/hr/missions/[id]/edit/page.jsx b/Frontend/app/hr/missions/[id]/edit/page.jsx new file mode 100644 index 0000000..34f47a6 --- /dev/null +++ b/Frontend/app/hr/missions/[id]/edit/page.jsx @@ -0,0 +1,226 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { motion } from 'framer-motion'; +import { ArrowLeft, Save } from 'lucide-react'; +import useStore from '@/lib/store'; +import HRSidebar from '@/components/navigation/hr-sidebar'; +import CosmicCard from '@/components/ui/cosmic-card'; +import { categories, ranks } from '@/lib/mockData'; +import { toast } from 'sonner'; + +const icons = [ + 'FileText', + 'Users', + 'Code', + 'Presentation', + 'BookOpen', + 'Lightbulb', + 'Rocket', + 'TrendingUp', + 'Target', + 'Award', +]; + +export default function EditMissionPage() { + const params = useParams(); + const router = useRouter(); + const { user, isHR, missions, updateMission } = useStore(); + const [formData, setFormData] = useState(null); + + const mission = missions.find((m) => m.id === parseInt(params.id)); + + useEffect(() => { + if (!user) { + router.push('/login'); + } else if (!isHR) { + router.push('/dashboard'); + } else if (mission) { + setFormData({ + title: mission.title, + description: mission.description, + category: mission.category, + experienceReward: mission.experienceReward, + manaReward: mission.manaReward, + requiredRank: mission.requiredRank, + icon: mission.icon, + skills: mission.skills || [], + }); + } + }, [user, isHR, mission, router]); + + if (!user || !isHR || !mission || !formData) return null; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!formData.title || !formData.description) { + toast.error('Заполните все обязательные поля'); + return; + } + + updateMission(mission.id, formData); + toast.success('Миссия обновлена успешно!'); + router.push('/hr/missions'); + }; + + const handleChange = (field, value) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + return ( +
+ + +
+
+ +
+ + +
+

Редактирование миссии

+

Внесите изменения в миссию

+
+ + +
+
+ + handleChange('title', e.target.value)} + placeholder="Например: Загрузить резюме" + className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors" + required + /> +
+ +
+ +