diff --git a/frontend/app/[locale]/about/page.tsx b/frontend/app/[locale]/about/page.tsx index 67225eef..5165b909 100644 --- a/frontend/app/[locale]/about/page.tsx +++ b/frontend/app/[locale]/about/page.tsx @@ -1,15 +1,29 @@ +import { getPlatformStats } from "@/lib/about/stats" +import { getSponsors } from "@/lib/about/github-sponsors" + import { HeroSection } from "@/components/about/HeroSection" +import { TopicsSection } from "@/components/about/TopicsSection" import { FeaturesSection } from "@/components/about/FeaturesSection" import { PricingSection } from "@/components/about/PricingSection" import { CommunitySection } from "@/components/about/CommunitySection" -export default function AboutPage() { +export default async function AboutPage() { + const [stats, sponsors] = await Promise.all([ + getPlatformStats(), + getSponsors() + ]) + return ( -
- +
+ + + - + +
) -} +} \ No newline at end of file diff --git a/frontend/app/[locale]/contacts/page.tsx b/frontend/app/[locale]/contacts/page.tsx index 42eb7a9a..995129f7 100644 --- a/frontend/app/[locale]/contacts/page.tsx +++ b/frontend/app/[locale]/contacts/page.tsx @@ -44,4 +44,4 @@ export default function ContactsPage() {
); -} +} \ No newline at end of file diff --git a/frontend/app/globals.css b/frontend/app/globals.css index e88c9cb0..957b9c02 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -199,3 +199,32 @@ --border: color-mix(in oklab, var(--foreground) 18%, var(--background)); --input: color-mix(in oklab, var(--foreground) 18%, var(--background)); } + +/* about-community */ +@keyframes scroll { + from { transform: translateX(0); } + to { transform: translateX(-100%); } +} + +.animate-scroll { + animation: scroll 40s linear infinite; +} + +.pause-on-hover:hover .animate-scroll { + animation-play-state: paused; +} + +/* Vertical Marquee Animation */ +@keyframes marquee-vertical { + 0% { transform: translateY(0); } + 100% { transform: translateY(-50%); } /* Рухаємось на 50%, бо контент дубльовано */ +} + +.animate-marquee-vertical { + animation: marquee-vertical var(--duration, 40s) linear infinite; +} + +/* Зупинка при наведенні мишкою, щоб роздивитися */ +.hover\:pause:hover .animate-marquee-vertical { + animation-play-state: paused; +} \ No newline at end of file diff --git a/frontend/components/about/CommunitySection.tsx b/frontend/components/about/CommunitySection.tsx index 510884ca..9c222e9f 100644 --- a/frontend/components/about/CommunitySection.tsx +++ b/frontend/components/about/CommunitySection.tsx @@ -1,90 +1,85 @@ "use client" -import { motion } from "framer-motion" -import { MessageCircle } from "lucide-react" -import { useTranslations } from "next-intl" +import { MessageCircle, Github, ArrowRight, ExternalLink } from "lucide-react" +import { TESTIMONIALS, type Testimonial } from "@/data/about" -const testimonialData = [ - { - name: "Alex Chen", - role: "Frontend Engineer @ Meta", - avatar: "AC", - platform: "LinkedIn", - }, - { - name: "Sarah Johnson", - role: "Senior SWE @ Google", - avatar: "SJ", - platform: "Twitter", - }, - { - name: "Marcus Williams", - role: "Backend Developer @ Stripe", - avatar: "MW", - platform: "Twitter", - }, - { - name: "Emily Park", - role: "Full Stack @ Vercel", - avatar: "EP", - platform: "LinkedIn", - }, - { - name: "David Kim", - role: "Staff Engineer @ Netflix", - avatar: "DK", - platform: "Twitter", - }, - { - name: "Lisa Thompson", - role: "Engineering Manager @ Amazon", - avatar: "LT", - platform: "LinkedIn", - }, -] +import Link from "next/link" export function CommunitySection() { - const t = useTranslations("about.community") + return ( +
+ +
- const testimonials = testimonialData.map((data, index) => ({ - ...data, - content: t(`testimonials.${index}`), - })) +
+ +
+
+ Community Love +
+ +

+ Approved by Survivors +

+

+ Join thousands of developers who stopped guessing and started shipping. Real feedback from real engineers. +

+
- return ( -
-
- -

- {t("title")} -

-

- {t("subtitle")} -

-
+
+ +
+
-
-
-
+
+
+ {TESTIMONIALS.map((testimonial, index) => ( + + ))} +
+ +
+
+ +
+ +
+ + Have a success story or feature request? + +
+ + + + Join Discussion + + + + +

+ We read every single thread +

+
-
- - {[...testimonials, ...testimonials].map((testimonial, index) => ( - - ))} - -
-
) @@ -96,34 +91,34 @@ function TestimonialCard({ avatar, content, platform, -}: { - name: string - role: string - avatar: string - content: string - platform: string -}) { + icon: Icon, + color +}: Testimonial) { return ( -
-
+
+
-
+
{avatar}
-
{name}
-
{role}
+
{name}
+
{role}
-
- +
+
-

{content}

+

+ "{content}" +

-
via {platform}
+
+ via {platform} +
) -} +} \ No newline at end of file diff --git a/frontend/components/about/FeaturesSection.tsx b/frontend/components/about/FeaturesSection.tsx index 70ecd3d3..8faf9652 100644 --- a/frontend/components/about/FeaturesSection.tsx +++ b/frontend/components/about/FeaturesSection.tsx @@ -1,175 +1,335 @@ "use client" -import type React from "react" -import { Link } from '@/i18n/routing'; -import { MessageCircle, Brain, Trophy, User, ShoppingBag, Star, Flame, Target } from "lucide-react" -import { useTranslations } from "next-intl" - -interface FeatureCardProps { - icon: React.ReactNode - title: string - description: string - href: string - className?: string - children?: React.ReactNode -} - -function FeatureCard({ icon, title, description, href, className = "", children }: FeatureCardProps) { - const t = useTranslations("about.features") - - return ( -
-
-
- -
-
-
- {icon} -
-
- -
-

{title}

-

{description}

- {children} -
+import { useState } from "react" +import { motion, AnimatePresence } from "framer-motion" +import { Link } from "@/i18n/routing" +import { MessageCircle, Brain, Trophy, User, ShoppingBag } from "lucide-react" - - {t("learnMore")} - -
-
- ) +const tMock = (key: string) => { + const map: any = { + "title": "Everything you need to", + "titleHighlight": "get hired", + "subtitle": "Stop searching for scattered info. We curated the ultimate toolkit to crack technical interviews.", + + "qa.title": "Q&A", + "qa.description": "No fluff. Just a massive library of real interview questions with precise, recruiter-approved answers.", + + "quiz.title": "Quizzes", + "quiz.description": "Validate your confidence. Fast-paced interactive quizzes to spot your weak points before the interviewer does.", + + "leaderboard.title": "Leaderboard", + "leaderboard.description": "Gamify your prep. Earn points for every correct answer, keep your streak alive, and rank up against others.", + + "profile.title": "Analytics", + "profile.description": "Don't fly blind. Visualize your progress with detailed charts to see exactly which topics you've mastered.", + + "shop.title": "Shop", + "shop.description": "Upgrade your setup. High-quality developer apparel, desk accessories, and digital assets available for purchase.", + } + return map[key] || key } -function ProgressBar({ label, value, color }: { label: string; value: number; color: string }) { - return ( -
-
- {label} - {value}% -
-
-
-
-
- ) -} +export function FeaturesSection() { + const t = tMock + const [activeTab, setActiveTab] = useState("qa") -function AchievementBadge({ icon, label }: { icon: React.ReactNode; label: string }) { - return ( -
-
- {icon} -
- {label} -
- ) -} + const features = [ + { id: "qa", icon: MessageCircle, href: "/q&a" }, + { id: "quiz", icon: Brain, href: "/quizzes" }, + { id: "leaderboard", icon: Trophy, href: "/leaderboard" }, + { id: "profile", icon: User, href: "/profile" }, + { id: "shop", icon: ShoppingBag, href: "/shop" }, + ] -export function FeaturesSection() { - const t = useTranslations("about.features") - const tProfile = useTranslations("about.features.profile") + const activeFeature = features.find((f) => f.id === activeTab) + const activeHref = activeFeature ? activeFeature.href : "/" return ( -
- -
-
-
+
+ +
+
-
-
-

- {t("title")} {t("titleHighlight")} +
+ +
+

+ {t("title")} {t("titleHighlight")}

-

+

{t("subtitle")}

-
- - } - title={t("qa.title")} - description={t("qa.description")} - href="/q&a" - className="lg:row-span-1" +
+ +
- } - title={t("quiz.title")} - description={t("quiz.description")} - href="/quiz" - className="lg:row-span-1" - /> - - } - title={t("profile.title")} - description={t("profile.description")} - href="/profile" - className="md:col-span-2 lg:col-span-1 lg:row-span-2" - > -
-
-
- JD + +
+ +
+
+
+
+
-
-

John Doe

-

{tProfile("level")}

-
-
-
-

{tProfile("achievements")}

-
- } label={tProfile("badges.star")} /> - } label={tProfile("badges.streak")} /> - } label={tProfile("badges.focus")} /> - } label={tProfile("badges.champ")} /> + +
+ https:// + devlovers.net/{activeTab}
-
-

{tProfile("progress")}

- - - + +
+ + +
+ +
+ {activeTab === 'qa' && } + {activeTab === 'quiz' && } + {activeTab === 'leaderboard' && } + {activeTab === 'profile' && } + {activeTab === 'shop' && } +
+ +
+ +
- - - } - title={t("leaderboard.title")} - description={t("leaderboard.description")} - href="/leaderboard" - className="lg:row-span-1" - /> + +
- } - title={t("shop.title")} - description={t("shop.description")} - href="/shop" - className="lg:row-span-1" - /> +
+ {features.map((feature) => { + const isActive = activeTab === feature.id + return ( + + ) + })} +
+ +
+ + +

+ {t(`${activeTab}.description`)} +

+
+

) } + +function QAVisual() { + return ( +
+ +
+
+
Question
+
+
+
+ + + +
+ + + +
+
+
Correct Answer
+
+
+
+
+ +
+ ) +} + +function QuizVisual() { + return ( +
+ + + + +
+
+
+
+
+
+ +
+ ) +} + +function LeaderboardVisual() { + return ( +
+ +
3
+
+ + +
1
+
+ +
2
+
+
+ ) +} + +function ProfileVisual() { + return ( +
+
+
+
+
+
+
+
+
+
+ JS + 85% +
+
+ +
+
+ React + 60% +
+
+ +
+
+
+ ) +} + +function ShopVisual() { + return ( +
+ {[1, 2].map((i) => ( + +
+ +
+
+
+
+
$25
+
+ + ))} +
+ ) +} \ No newline at end of file diff --git a/frontend/components/about/HeroSection.tsx b/frontend/components/about/HeroSection.tsx index 4ce588ad..bfeecf65 100644 --- a/frontend/components/about/HeroSection.tsx +++ b/frontend/components/about/HeroSection.tsx @@ -1,66 +1,157 @@ "use client" -import { motion } from "framer-motion" -import { Heart, Sparkles } from "lucide-react" -import { StatsSection } from "@/components/about/StatsSection" -import { useTranslations } from "next-intl" +import { useRef } from "react" +import { motion, useMotionTemplate, useMotionValue } from "framer-motion" +import { CheckCircle, Users, Star, Linkedin, ArrowDown } from "lucide-react" +import { InteractiveGame } from "./InteractiveGame" +import type { PlatformStats } from "@/lib/about/stats" -export function HeroSection() { - const t = useTranslations("about.hero") +export function HeroSection({ stats }: { stats?: PlatformStats }) { + const containerRef = useRef(null) + + const mouseX = useMotionValue(0) + const mouseY = useMotionValue(0) - return ( -
-
-
+ function handleMouseMove({ currentTarget, clientX, clientY }: React.MouseEvent) { + const { left, top } = currentTarget.getBoundingClientRect() + mouseX.set(clientX - left) + mouseY.set(clientY - top) + } + + const data = stats || { + questionsSolved: "850+", + githubStars: "120+", + activeUsers: "200+", + linkedinFollowers: "1.3k+" + } + + return ( +
+ +
+
+
+ + +
+ + +
+
+
+ +
+ +
+ + + + + + +
+ +
+ +
+
- + + Debug your skills + before the recruiter does. + + + + Stop guessing. We decoded the chaotic interview process into a structured roadmap. Compile your scattered knowledge into a production-ready skillset and land that offer. + + + + + + + + + + - - {"{"} - - - - {"}"} - - - - - {t("badge")} - - -

- {t("title")}
- {t("titleHighlight")} -

- -

- {t("description")} -

- -
- - + -
+
+ +
+ + + + + + +
+ +
+
+ ) +} + +function GlassWidget({ icon: Icon, color, bg, label, value }: any) { + return ( + +
+ +
+
+
{label}
+
{value}
+
+
) } + +function MobileStatItem({ icon: Icon, color, bg, label, value }: any) { + return ( +
+
+ +
+
+
{value}
+
{label}
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/components/about/InteractiveGame.tsx b/frontend/components/about/InteractiveGame.tsx new file mode 100644 index 00000000..e36be3ed --- /dev/null +++ b/frontend/components/about/InteractiveGame.tsx @@ -0,0 +1,276 @@ +"use client" + +import { useState, useEffect, useRef, useCallback } from "react" +import { motion, AnimatePresence } from "framer-motion" +import { Heart, Bug, Play, X, RotateCcw, Zap } from "lucide-react" + +export function InteractiveGame() { + const [mode, setMode] = useState<'idle' | 'preview' | 'playing'>('idle') + const [gameOver, setGameOver] = useState(false) + const [score, setScore] = useState(0) + + const [highScore, setHighScore] = useState(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('devlovers_highscore') + return saved ? parseInt(saved, 10) : 0 + } + return 0 + }) + + const [isJumping, setIsJumping] = useState(false) + const [gameSpeed, setGameSpeed] = useState(1.8) + const [resetKey, setResetKey] = useState(0) + + const pillRef = useRef(null) + const requestRef = useRef(null) + const playerRef = useRef(null) + const obstacleRef = useRef(null) + + const exitGame = useCallback(() => { + setMode('idle') + setGameOver(false) + setScore(0) + setGameSpeed(1.8) + if (requestRef.current !== null) cancelAnimationFrame(requestRef.current) + }, []) + + const handleRetry = useCallback(() => { + setGameOver(false) + setScore(0) + setGameSpeed(1.8) + setResetKey(prev => prev + 1) + }, []) + + const handleGameOver = useCallback(() => { + setGameOver(true) + setHighScore(prev => { + const newHigh = Math.max(prev, score) + if (typeof window !== 'undefined') { + localStorage.setItem('devlovers_highscore', newHigh.toString()) + } + return newHigh + }) + }, [score]) + + const jump = useCallback(() => { + if (!isJumping && mode === 'playing' && !gameOver) { + setIsJumping(true) + setTimeout(() => setIsJumping(false), 550) + } + }, [isJumping, mode, gameOver]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (pillRef.current && !pillRef.current.contains(e.target as Node)) { + exitGame() + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, [exitGame]) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (mode !== 'playing') return + if (e.code === "Space" || e.code === "ArrowUp") { + e.preventDefault() + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + gameOver ? handleRetry() : jump() + } + if (e.code === "Escape") exitGame() + } + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [mode, gameOver, jump, handleRetry, exitGame]) + + useEffect(() => { + if (mode !== 'playing' || gameOver) return + + const checkFrame = () => { + const player = playerRef.current + const obstacle = obstacleRef.current + + if (player && obstacle) { + const p = player.getBoundingClientRect() + const o = obstacle.getBoundingClientRect() + + const paddingX = 16 + const paddingY = 12 + + const isColliding = + p.right - paddingX > o.left + paddingX && + p.left + paddingX < o.right - paddingX && + p.bottom - paddingY > o.top + paddingY + + if (isColliding) { + handleGameOver() + return + } + + setScore(s => { + const newScore = s + 1 + if (newScore % 300 === 0) { + setGameSpeed(prev => Math.max(0.7, prev * 0.95)) + } + return newScore + }) + } + requestRef.current = requestAnimationFrame(checkFrame) + } + requestRef.current = requestAnimationFrame(checkFrame) + return () => { if (requestRef.current !== null) cancelAnimationFrame(requestRef.current) } + }, [mode, gameOver, handleGameOver, resetKey]) + + return ( + mode === 'idle' && setMode('preview')} + onHoverEnd={() => mode === 'preview' && setMode('idle')} + className="relative mb-14 select-none border border-neutral-200 dark:border-white/10 bg-white/80 dark:bg-[#0a0a0a]/80 backdrop-blur-xl shadow-2xl z-50 mx-auto group overflow-hidden" + onClick={() => mode !== 'playing' && setMode('playing')} + > + + {mode === 'idle' ? ( + +
+ + + DevLovers Arcade + +
+
+ + READY +
+
+ ) : ( + +
+
+
+
+ Status + + {gameOver ? "CRASHED" : "RUNNING"} + +
+
+ +
+
+ High + + {Math.floor(highScore / 5).toString().padStart(4, '0')} + +
+
+
+ Score + + {Math.floor(score / 5).toString().padStart(5, '0')} + +
+
+ + +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ + {mode === 'playing' && ( +
+ +
+ )} + + {mode === 'preview' && ( + +
+ + Click to Start +
+
+ )} + + {gameOver && ( + +
+ + System Failure +
+
+ +
+
+ )} + +
+
+
+ )} + + + + + ) +} diff --git a/frontend/components/about/PricingSection.tsx b/frontend/components/about/PricingSection.tsx index fac09d93..15b74862 100644 --- a/frontend/components/about/PricingSection.tsx +++ b/frontend/components/about/PricingSection.tsx @@ -1,216 +1,150 @@ "use client" -import { useState, useEffect, useRef } from "react" -import { motion, AnimatePresence, useInView } from "framer-motion" -import { Heart, Sparkles, Coffee } from "lucide-react" -import { useTranslations } from "next-intl" - -function PriceEvolution() { - const containerRef = useRef(null) - const isInView = useInView(containerRef, { once: true, amount: 0.5 }) - - const [displayValue, setDisplayValue] = useState("$14.99") - const [isFinal, setIsFinal] = useState(false) - const [colorState, setColorState] = useState<"normal" | "rising" | "chaos">("normal") - - useEffect(() => { - if (!isInView) return - - let interval: NodeJS.Timeout - let step = 0 - - const sequence = async () => { - await new Promise(r => setTimeout(r, 1500)) - - setColorState("rising") - let currentValue = 14.99 - - await new Promise(resolve => { - interval = setInterval(() => { - currentValue += Math.random() * 10 - setDisplayValue(`$${Math.floor(currentValue)}`) - - step++ - if (step > 15) { - clearInterval(interval) - resolve() - } - }, 50) - }) - - setColorState("chaos") - const chars = "!@#$%^&*?<>~" - - await new Promise(resolve => { - let chaosStep = 0 - interval = setInterval(() => { - const randomStr = Array(3).fill(0).map(() => chars[Math.floor(Math.random() * chars.length)]).join("") - setDisplayValue(`$${randomStr}`) - - chaosStep++ - if (chaosStep > 10) { - clearInterval(interval) - resolve() - } - }, 50) - }) - - setIsFinal(true) - } - - sequence() - - return () => clearInterval(interval) - }, [isInView]) +import { motion } from "framer-motion" +import { Check, Heart, X, Sparkles, Server, ArrowRight } from "lucide-react" +import type { Sponsor } from "@/lib/about/github-sponsors" +import { SponsorsWall } from "./SponsorsWall" +import Link from "next/link" +interface PricingSectionProps { + sponsors?: Sponsor[] +} +export function PricingSection({ sponsors = [] }: PricingSectionProps) { return ( -
- - {!isFinal ? ( - - - {displayValue} - - - ) : ( - +
+ +
+
+ - - $0 - - - - + No Hidden Fees - )} - -
- ) -} - -export function PricingSection() { - const t = useTranslations("about.pricing") - const tFeatures = useTranslations("about.pricing.features") - - const featureKeys = [ - "unlimitedQuestions", - "fullQuizAccess", - "globalLeaderboard", - "progressTracking", - "communityChallenges", - "mobileFriendly" - ] as const - - return ( -
-
- -
- - -

- {t("title")}
- {t("titleHighlight")} +

+ Invest in your brain,
+ not our subscriptions.

-

- {t("subtitle")} +

+ We believe knowledge should be accessible. So we don't sell courses. But servers heat up and coffee runs out. The choice is yours.

-
- - -
- - {t("badge")} - -
- -
-
- -
-

{t("monthlyPrice")}

+
- +
+ + +
+

Junior Engineer

+

For those who want an offer, not expenses.

+
+
+ $0 + / forever +
+ +
    + {[ + "Unlimited Questions", + "Full Quiz Access", + "No Credit Card Required", + "0% Guilt Trip", + ].map((item) => ( +
  • +
    + +
    + {item} +
  • + ))} +
  • +
    + +
    + Personal Yacht +
  • +
-
-

- {t("free")} -

-
-
+ + Start Learning + + -
+ +
+ High Impact +
-
- {featureKeys.map((featureKey) => ( -
-
- -
- {tFeatures(featureKey)} +
+

+ Open Source Hero + +

+

For those who already landed an offer thanks to us.

+
+
+ $$$ + / karma points +
+ +
    + {[ + "Keep Servers Alive", + "Buy Coffee for Mentors", + "Profile Badge (Big Flex)", + "Warm Fuzzy Feeling", + ].map((item) => ( +
  • +
    +
    - ))} -
- -
- + {item} + + ))} +
  • +
    + +
    + We actually pay for Drizzle +
  • + + + + Support the Project + + - - - {t("coffee")} - -
    +
    + +

    + *No developers were harmed in the making of this pricing table. Only caffeine levels were impacted. +

    -

    - {t("noCard")} -

    -
    -
    - + +
    ) -} +} \ No newline at end of file diff --git a/frontend/components/about/SponsorsWall.tsx b/frontend/components/about/SponsorsWall.tsx new file mode 100644 index 00000000..1b2b30d8 --- /dev/null +++ b/frontend/components/about/SponsorsWall.tsx @@ -0,0 +1,110 @@ +"use client" + +import { motion } from "framer-motion" +import Image from "next/image" +import Link from "next/link" +import { Plus, Crown, Sparkles } from "lucide-react" +import { cn } from "@/lib/utils" +import type { Sponsor } from "@/lib/about/github-sponsors" + +interface SponsorsWallProps { + sponsors?: Sponsor[] +} + +export function SponsorsWall({ sponsors = [] }: SponsorsWallProps) { + + const displaySponsors = sponsors + + return ( +
    +
    + + + Latest Contributors + +
    + + +
    + {displaySponsors.map((sponsor) => ( + + ))} + + {displaySponsors.length === 0 && ( +
    + You? +
    + )} +
    + +
    + + + + +
    + Your Spot +
    +
    + + + + Join the club + + + + +

    + 100% of funds go to server costs & coffee +

    + +
    + ) +} + +function SponsorItem({ sponsor }: { sponsor: Sponsor }) { + const ringColor = sponsor.tierColor === 'gold' ? 'ring-[#1e5eff] dark:ring-[#ff2d55]' : + sponsor.tierColor === 'silver' ? 'ring-gray-400' : 'ring-orange-700/50' + + return ( + +
    + {sponsor.login} + + {sponsor.tierColor === 'gold' && ( +
    + +
    + )} +
    + +
    + @{sponsor.login} + | + + {sponsor.tierName} + +
    + + ) +} \ No newline at end of file diff --git a/frontend/components/about/StatsSection.tsx b/frontend/components/about/StatsSection.tsx deleted file mode 100644 index b827181f..00000000 --- a/frontend/components/about/StatsSection.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { motion } from "framer-motion" -import { Star, Linkedin, Users, Terminal } from "lucide-react" -import { useTranslations } from "next-intl" - -const GITHUB_USERNAME = "DevLoversTeam" -const GITHUB_REPO = "devlovers.net" -const LINKEDIN_COUNT = "1.2k+" -const ACTIVE_USERS = "1" -const QUESTIONS_SOLVED = "0" - -function formatCount(num: number): string { - if (num >= 1000) return (num / 1000).toFixed(1) + "k" - return num.toString() -} - -export function StatsSection() { - const t = useTranslations("about.stats") - const [githubStars, setGithubStars] = useState("...") - - useEffect(() => { - fetch(`https://api.github.com/repos/${GITHUB_USERNAME}/${GITHUB_REPO}`) - .then(res => res.json()) - .then(data => { - const stars = data.stargazers_count - ? formatCount(data.stargazers_count) - : "2.5k" - setGithubStars(stars) - }) - .catch(() => setGithubStars("2.5k")) - }, []) - - const stats = [ - { key: "stars", value: githubStars, label: t("githubStars"), icon: Star }, - { key: "linkedin", value: LINKEDIN_COUNT, label: t("followers"), icon: Linkedin }, - { key: "users", value: ACTIVE_USERS, label: t("activeDevs"), icon: Users }, - { key: "solved", value: QUESTIONS_SOLVED, label: t("solved"), icon: Terminal }, - ] - - return ( -
    -
    - {stats.map((item, index) => { - const Icon = item.icon - return ( - -
    - -
    - -
    - {item.value} -
    - -
    - {item.label} -
    -
    - ) - })} -
    -
    - ) -} diff --git a/frontend/components/about/TopicsSection.tsx b/frontend/components/about/TopicsSection.tsx new file mode 100644 index 00000000..e3e82bd3 --- /dev/null +++ b/frontend/components/about/TopicsSection.tsx @@ -0,0 +1,98 @@ +"use client" + +import { motion } from "framer-motion" +import { ArrowUpRight } from "lucide-react" +import { TOPICS, type Topic } from "@/data/about" +import Image from "next/image" +import Link from "next/link" + +export function TopicsSection() { + return ( +
    +
    + +
    +
    + + / The Ecosystem + +

    + Master your
    + + entire stack + +

    +
    + +

    + From frontend frameworks to backend logic.
    We cover the key technologies for 2026. +

    +
    + +
    + {TOPICS.map((topic, i) => ( + + ))} +
    + +
    +
    + ) +} + +function TopicCard({ topic, index }: { topic: Topic, index: number }) { + return ( + + +
    + +
    +
    + {topic.name} +
    + +
    + +
    +

    + {topic.name} +

    +

    + {topic.questions} +

    +
    + +
    +
    + + + ) +} \ No newline at end of file diff --git a/frontend/components/ui/github-star-button.tsx b/frontend/components/ui/github-star-button.tsx new file mode 100644 index 00000000..e69de29b diff --git a/frontend/data/about.ts b/frontend/data/about.ts new file mode 100644 index 00000000..e8ad28a5 --- /dev/null +++ b/frontend/data/about.ts @@ -0,0 +1,146 @@ +import { Twitter, Linkedin, Github } from "lucide-react" + +export const TESTIMONIALS = [ + { + name: "Alex Chen", + role: "Frontend @ Meta", + avatar: "AC", + content: "Cheap therapy for React devs.", + platform: "Twitter", + icon: Twitter, + color: "text-sky-500 bg-sky-500/10" + }, + { + name: "Sarah J.", + role: "Senior SWE @ Google", + avatar: "SJ", + content: "Harder than my actual interview. 10/10.", + platform: "LinkedIn", + icon: Linkedin, + color: "text-blue-600 bg-blue-600/10" + }, + { + name: "git_push_force", + role: "Open Source Contributor", + avatar: "GP", + content: "Found a bug in the quiz, reported it, got points. Now I'm addicted to fixing your typos.", + platform: "GitHub", + icon: Github, + color: "text-gray-900 dark:text-white bg-gray-500/10" + }, + { + name: "Emily Park", + role: "Full Stack @ Vercel", + avatar: "EP", + content: "This is the only place where 'centering a div' is explained like I'm 5. Bless you.", + platform: "Twitter", + icon: Twitter, + color: "text-sky-500 bg-sky-500/10" + }, + { + name: "David Kim", + role: "Staff Engineer @ Netflix", + avatar: "DK", + content: "I use the Q&A section to win arguments with my juniors. Don't tell them.", + platform: "LinkedIn", + icon: Linkedin, + color: "text-blue-600 bg-blue-600/10" + }, +] + +export const TOPICS = [ + { + id: "git", + name: "Git & Version Control", + questions: "90+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/git/git-original.svg", + color: "group-hover:border-[#F05032]/50 group-hover:bg-[#F05032]/10", + glow: "bg-[#F05032]", + href: "/q&a" + }, + { + id: "html", + name: "HTML5 & Semantic", + questions: "120+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/html5/html5-original.svg", + color: "group-hover:border-[#E34F26]/50 group-hover:bg-[#E34F26]/10", + glow: "bg-[#E34F26]", + href: "/q&a/?category=html" + }, + { + id: "css", + name: "CSS3 & Responsive", + questions: "180+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/css3/css3-original.svg", + color: "group-hover:border-[#1572B6]/50 group-hover:bg-[#1572B6]/10", + glow: "bg-[#1572B6]", + href: "/q&a/?category=css" + }, + { + id: "js", + name: "JavaScript (ES6+)", + questions: "450+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/javascript/javascript-original.svg", + color: "group-hover:border-[#F7DF1E]/50 group-hover:bg-[#F7DF1E]/10", + glow: "bg-[#F7DF1E]", + href: "/q&a/?category=javascript" + }, + { + id: "ts", + name: "TypeScript", + questions: "210+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/typescript/typescript-original.svg", + color: "group-hover:border-[#3178C6]/50 group-hover:bg-[#3178C6]/10", + glow: "bg-[#3178C6]", + href: "/q&a/?category=typescript" + }, + { + id: "react", + name: "React.js Ecosystem", + questions: "320+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/react/react-original.svg", + color: "group-hover:border-[#61DAFB]/50 group-hover:bg-[#61DAFB]/10", + glow: "bg-[#61DAFB]", + href: "/q&a/?category=react" + }, + { + id: "next", + name: "Next.js & SSR", + questions: "140+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/nextjs/nextjs-original.svg", + color: "group-hover:border-black/50 dark:group-hover:border-white/50 group-hover:bg-black/5 dark:group-hover:bg-white/10", + glow: "bg-black dark:bg-white", + className: "dark:invert", + href: "/q&a/?category=next" + }, + { + id: "vue", + name: "Vue.js", + questions: "110+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/vuejs/vuejs-original.svg", + color: "group-hover:border-[#4FC08D]/50 group-hover:bg-[#4FC08D]/10", + glow: "bg-[#4FC08D]", + href: "/q&a/?category=vue" + }, + { + id: "angular", + name: "Angular", + questions: "95+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/angularjs/angularjs-original.svg", + color: "group-hover:border-[#DD0031]/50 group-hover:bg-[#DD0031]/10", + glow: "bg-[#DD0031]", + href: "/q&a/?category=angular" + }, + { + id: "node", + name: "Node.js & Backend", + questions: "150+ Questions", + icon: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/nodejs/nodejs-original.svg", + color: "group-hover:border-[#339933]/50 group-hover:bg-[#339933]/10", + glow: "bg-[#339933]", + href: "/q&a/?category=node" + }, +] + +export type Topic = typeof TOPICS[number]; +export type Testimonial = typeof TESTIMONIALS[number]; \ No newline at end of file diff --git a/frontend/lib/about/github-sponsors.ts b/frontend/lib/about/github-sponsors.ts new file mode 100644 index 00000000..be263e4c --- /dev/null +++ b/frontend/lib/about/github-sponsors.ts @@ -0,0 +1,88 @@ +import "server-only" + +export interface Sponsor { + login: string + name: string + avatarUrl: string + tierName: string + tierColor: "gold" | "silver" | "bronze" + monthlyPrice: number +} + +function getTierDetails(amount: number): { name: string; color: "gold" | "silver" | "bronze" } { + if (amount >= 100) return { name: "🏆 Core Supporter", color: "gold" } + if (amount >= 50) return { name: "🎓 Impact Support", color: "silver" } + if (amount >= 25) return { name: "🧠 Community Support", color: "silver" } + if (amount >= 10) return { name: "🚀 Early Support", color: "bronze" } + return { name: "☕ Coffee Support", color: "bronze" } +} + +export async function getSponsors(): Promise { + const token = process.env.GITHUB_SPONSORS_TOKEN + + if (!token) { + console.warn("⚠️ GITHUB_SPONSORS_TOKEN is missing.") + return [] + } + + const query = ` + query { + organization(login: "DevLoversTeam") { + sponsorshipsAsMaintainer(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, includePrivate: false) { + nodes { + tier { monthlyPriceInDollars } + sponsorEntity { + ... on User { login name avatarUrl } + ... on Organization { login name avatarUrl } + } + } + } + } + } + ` + + try { + const res = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query }), + cache: "no-store" // Завжди свіжі дані + }) + + const json = await res.json() + + if (json.errors) { + console.error("❌ GitHub API Error:", json.errors[0].message) + return [] + } + + const rawNodes = json.data?.organization?.sponsorshipsAsMaintainer?.nodes || [] + + console.log(`✅ GitHub: Found ${rawNodes.length} sponsors for Organization`) + + const sponsors: Sponsor[] = rawNodes.map((node: any) => { + const price = node.tier?.monthlyPriceInDollars || 0 + const { name, color } = getTierDetails(price) + + if (!node.sponsorEntity) return null + + return { + login: node.sponsorEntity.login, + name: node.sponsorEntity.name || node.sponsorEntity.login, + avatarUrl: node.sponsorEntity.avatarUrl, + monthlyPrice: price, + tierName: name, + tierColor: color, + } + }).filter(Boolean) as Sponsor[] + + return sponsors + + } catch (error) { + console.error("❌ Failed to fetch sponsors:", error) + return [] + } +} \ No newline at end of file diff --git a/frontend/lib/about/stats.ts b/frontend/lib/about/stats.ts new file mode 100644 index 00000000..683ab6d4 --- /dev/null +++ b/frontend/lib/about/stats.ts @@ -0,0 +1,64 @@ +import { db } from '@/db' +import { users } from '@/db/schema/users' +import { quizAttempts } from '@/db/schema/quiz' +import { count } from 'drizzle-orm' +import { unstable_cache } from 'next/cache' + +export interface PlatformStats { + githubStars: string + linkedinFollowers: string + activeUsers: string + questionsSolved: string +} + +const formatMetric = (n: number) => { + if (n >= 1000) return (n / 1000).toFixed(1) + 'k+' + return n.toString() +} + +export const getPlatformStats = unstable_cache( + async (): Promise => { + let stars = 125 + try { + const headers: HeadersInit = {} + if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}` + + const res = await fetch('https://api.github.com/repos/DevLoversTeam/devlovers.net', { + headers, + cache: 'no-store' + }) + + if (res.ok) { + const data = await res.json() + stars = data.stargazers_count + } + } catch (e) { + console.error("GitHub Fetch Error:", e) + } + + const linkedinCount = process.env.LINKEDIN_FOLLOWER_COUNT ? parseInt(process.env.LINKEDIN_FOLLOWER_COUNT) : 1342 + + let totalUsers = 243 + let solvedTests = 1890 + try { + const [[u], [q]] = await Promise.all([ + db.select({ value: count() }).from(users), + db.select({ value: count() }).from(quizAttempts) + ]) + + if (u) totalUsers = u.value + if (q) solvedTests = q.value + } catch (e) { + console.error("DB Fetch Error:", e) + } + + return { + githubStars: formatMetric(stars), + linkedinFollowers: formatMetric(linkedinCount), + activeUsers: formatMetric(totalUsers), + questionsSolved: formatMetric(solvedTests) + } + }, + ['platform-stats'], + { revalidate: 3600 } +) \ No newline at end of file diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 37bb7f58..e021c2e1 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -36,8 +36,18 @@ const nextConfig: NextConfig = { hostname: "api.dicebear.com", pathname: "/**", }, + { + protocol: "https", + hostname: "github.com", + pathname: "/**", + }, + { + protocol: 'https', + hostname: 'cdn.jsdelivr.net', + pathname: '/**', + }, ], }, }; -export default withNextIntl(nextConfig); +export default withNextIntl(nextConfig); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a4f372be..3c41f9d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -48,9 +48,9 @@ "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^14.6.1", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^20", + "@types/node": "^20.19.30", "@types/nodemailer": "^7.0.4", - "@types/react": "^19", + "@types/react": "^19.2.8", "@types/react-dom": "^19", "drizzle-kit": "^0.18.1", "eslint": "^9", @@ -915,7 +915,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1223,7 +1222,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1267,7 +1265,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1318,6 +1315,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -1335,6 +1333,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1352,6 +1351,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1369,6 +1369,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1386,6 +1387,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1403,6 +1405,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1420,6 +1423,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1437,6 +1441,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1454,6 +1459,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1471,6 +1477,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1488,6 +1495,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1505,6 +1513,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1522,6 +1531,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1539,6 +1549,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1556,6 +1567,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1573,6 +1585,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1590,6 +1603,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1607,6 +1621,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1624,6 +1639,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1641,6 +1657,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1658,6 +1675,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1675,6 +1693,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -1692,6 +1711,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -1709,6 +1729,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -1726,6 +1747,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -1743,6 +1765,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -4577,7 +4600,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.1.tgz", "integrity": "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -5043,7 +5065,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5239,7 +5260,6 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5250,7 +5270,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5300,7 +5319,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -5949,7 +5967,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6363,7 +6380,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7914,7 +7930,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8100,7 +8115,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9698,7 +9712,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10504,7 +10517,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.1.3.tgz", "integrity": "sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.1.3", "@swc/helpers": "0.5.15", @@ -10628,6 +10640,16 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -10983,7 +11005,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", "integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.10.0", "pg-pool": "^3.11.0", @@ -11147,7 +11168,6 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz", "integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==", "license": "Unlicense", - "peer": true, "engines": { "node": ">=12" }, @@ -11314,7 +11334,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11324,7 +11343,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12309,7 +12327,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13167,7 +13184,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13352,7 +13368,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13930,7 +13945,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14309,7 +14323,6 @@ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index ea453a88..0f0a0651 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,9 +59,9 @@ "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^14.6.1", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^20", + "@types/node": "^20.19.30", "@types/nodemailer": "^7.0.4", - "@types/react": "^19", + "@types/react": "^19.2.8", "@types/react-dom": "^19", "drizzle-kit": "^0.18.1", "eslint": "^9",