Skip to content
Open
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
12 changes: 10 additions & 2 deletions app/ask/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ export const metadata: Metadata = {
description: "Семантический поиск по материалам StackMIREA: темы, практики, сравнение дисциплин и быстрый вход в нужные страницы."
};

export default function AskPage() {
interface AskPageProps {
searchParams?: {
q?: string;
};
}

export default function AskPage({ searchParams }: AskPageProps) {
const initialQuery = typeof searchParams?.q === "string" ? searchParams.q : "";

return (
<div className="mx-auto w-full max-w-[1440px] px-4 pb-20 pt-14 sm:px-6 lg:px-8">
<section className="relative overflow-hidden rounded-3xl border border-border/70 bg-card/65 px-6 py-12 sm:px-10">
Expand Down Expand Up @@ -39,7 +47,7 @@ export default function AskPage() {
</section>

<section className="mt-8">
<AskStackMirea />
<AskStackMirea initialQuery={initialQuery} />
</section>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
--border: 210 10% 87%;
--input: 210 10% 87%;
--ring: 0 0% 18%;
--site-header-height: 104px;
}

.dark {
Expand Down Expand Up @@ -68,6 +69,12 @@ body {
hsl(var(--background));
}

@media (min-width: 768px) {
:root {
--site-header-height: 96px;
}
}

@media (prefers-reduced-motion: no-preference) {
html[data-theme-transition="wave"]::view-transition-old(root),
html[data-theme-transition="wave"]::view-transition-new(root) {
Expand Down
446 changes: 282 additions & 164 deletions app/page.tsx

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions components/home/HomeSearchForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { Search } from "lucide-react";
import { useRouter } from "next/navigation";
import { type FormEvent, useState } from "react";

import { withBasePath } from "@/lib/utils";

interface HomeSearchFormProps {
defaultValue?: string;
}

export function HomeSearchForm({ defaultValue = "" }: HomeSearchFormProps) {
const router = useRouter();
const [query, setQuery] = useState(defaultValue);

function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();

const normalizedQuery = query.trim();
const url = normalizedQuery ? `${withBasePath("/ask")}?q=${encodeURIComponent(normalizedQuery)}` : withBasePath("/ask");

router.push(url);
}

return (
<form onSubmit={handleSubmit} className="mt-6 rounded-[24px] border border-[#d7e2ff] bg-white/92 p-3 shadow-[0_16px_40px_rgba(79,124,214,0.10)] dark:border-white/10 dark:bg-white/5">
<div className="flex flex-col gap-3 md:flex-row">
<label htmlFor="home-search-input" className="relative flex-1">
<span className="sr-only">Поиск по материалам StackMIREA</span>
<Search className="pointer-events-none absolute left-5 top-1/2 size-4 -translate-y-1/2 text-slate-400" />
<input
id="home-search-input"
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Где в материалах есть про KNN"
className="h-12 w-full rounded-[18px] border border-slate-200 bg-white pl-12 pr-4 text-sm text-slate-900 outline-none transition-colors placeholder:text-slate-400 focus:border-[#4f7cf6] dark:border-white/10 dark:bg-slate-950 dark:text-white dark:placeholder:text-slate-500"
/>
</label>

<button
type="submit"
className="inline-flex h-12 items-center justify-center rounded-[14px] bg-[#3575f6] px-6 text-sm font-semibold text-white transition-colors hover:bg-[#2868ea]"
>
Найти ответ
</button>
</div>
</form>
);
}
100 changes: 81 additions & 19 deletions components/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,91 @@
import Image from "next/image";
import Link from "next/link";

import { DEFAULT_BRANCH, REPO_NAME, REPO_OWNER, REPO_URL, SITE_NAME } from "@/lib/utils";
import siteLogo from "@/public/favicon.png";
import { DEFAULT_BRANCH, REPO_NAME, REPO_OWNER, REPO_URL, SITE_NAME, SITE_ORIGIN } from "@/lib/utils";

const CONTENT_LICENSE_URL = `${REPO_URL}/blob/${DEFAULT_BRANCH}/CC-BY-NC-SA-4.0`;
const footerGroups = [
{
title: "Продукт",
links: [
{ label: "Сообщить об ошибках", href: `${REPO_URL}/issues/new/choose` }
]
},
{
title: "О нас",
links: [
{ label: "Min’s collective site", href: `${SITE_ORIGIN}/StackMIREA/` },
{ label: "tg/min’s Collective", href: `${REPO_URL}#readme` }
]
},
{
title: "GitHub",
links: [
{ label: `${REPO_OWNER}/${REPO_NAME}`, href: REPO_URL }
]
},
{
title: "Социальные сети",
links: [
{ label: "tg/stackMirea", href: `${REPO_URL}/blob/${DEFAULT_BRANCH}/SUPPORT.md` }
]
}
] as const;

export function Footer() {
return (
<footer className="border-t border-border/70 bg-background/90">
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-2 px-4 py-6 text-center text-sm text-muted-foreground sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:px-6 sm:text-left lg:px-8">
<p className="break-words">{SITE_NAME} Документация</p>
<p className="break-words">
Весь контент сайта защищен лицензией{" "}
<Link href={CONTENT_LICENSE_URL} target="_blank" rel="noreferrer" className="transition-colors hover:text-foreground">
CC-BY-NC-SA-4.0
</Link>
.
</p>
<Link
href={REPO_URL}
target="_blank"
rel="noreferrer"
className="break-all transition-colors hover:text-foreground"
>
{REPO_OWNER}/{REPO_NAME}
</Link>
<footer className="mt-24 border-t border-border/70 bg-background/95">
<div className="mx-auto w-full max-w-[1240px] px-5 py-10 sm:px-8 lg:px-10">
<div className="grid gap-12 xl:grid-cols-[1.05fr_1fr] xl:items-start">
<div>
<Link href="/" className="inline-flex items-center gap-4">
<Image src={siteLogo} alt={`Логотип ${SITE_NAME}`} width={50} height={50} className="h-11 w-11 rounded-sm" />
<span className="text-[17px] font-medium text-foreground/55">{SITE_NAME}</span>
</Link>

<div className="mt-14 grid gap-8 sm:grid-cols-2 lg:grid-cols-4 lg:gap-10">
{footerGroups.map((group) => (
<div key={group.title}>
<h3 className="text-[28px] font-semibold tracking-tight text-foreground">{group.title}</h3>
<div className="mt-5 space-y-2.5">
{group.links.map((item) => (
<Link
key={item.href}
href={item.href}
target="_blank"
rel="noreferrer"
className="block text-[13px] text-foreground/40 underline decoration-foreground/20 underline-offset-4 transition-colors hover:text-foreground/70"
>
{item.label}
</Link>
))}
</div>
</div>
))}
</div>
</div>

<div className="flex flex-col xl:items-end">
<p className="text-[2rem] font-semibold tracking-[-0.04em] text-foreground sm:text-[2.35rem] lg:text-[44px] lg:leading-none">
Упрощаем обучение
</p>

<div className="mt-7 inline-flex min-h-[84px] w-full items-center justify-center rounded-[18px] bg-[#3575f6] px-7 py-5 text-center text-[22px] font-semibold uppercase tracking-[0.02em] text-white shadow-[0_20px_48px_rgba(53,117,246,0.24)] sm:text-[26px] lg:max-w-[620px] lg:text-[30px]">
от студентов для студентов
</div>
</div>
</div>

<div className="mt-10 flex justify-end">
<p className="text-right text-[11px] text-foreground/40">
© 2026 Min&apos;s collective · [Весь контент сайта защищен лицензией{" "}
<Link href={CONTENT_LICENSE_URL} target="_blank" rel="noreferrer" className="underline underline-offset-2 transition-colors hover:text-foreground/70">
CC-BY-NC-SA-4.0
</Link>
.]
</p>
</div>
</div>
</footer>
);
Expand Down
114 changes: 70 additions & 44 deletions components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,86 @@
import Link from "next/link";
"use client";

import Image from "next/image";
import { BookOpenText, BrainCircuit, Github, Home } from "lucide-react";
import Link from "next/link";
import { BookOpen, Home, Search } from "lucide-react";
import { usePathname } from "next/navigation";

import siteLogo from "@/public/favicon.png";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
import { REPO_URL, SITE_NAME } from "@/lib/utils";
import { SITE_NAME, cn } from "@/lib/utils";

const navigation = [
{
href: "/",
label: "Главная",
icon: Home
},
{
href: "/docs",
label: "Документация",
icon: BookOpen
},
{
href: "/ask",
label: "ИИ-Поиск",
icon: Search
}
] as const;

export function Header() {
const pathname = usePathname();

return (
<header className="sticky top-0 z-50 border-b border-border/80 bg-background/92 backdrop-blur-md">
<div className="mx-auto flex h-14 w-full max-w-[1440px] items-center justify-between px-4 sm:px-6 lg:px-8">
<div className="flex min-w-0 items-center gap-4">
<Link href="/" className="inline-flex min-w-0 items-center gap-2 text-sm font-semibold tracking-tight text-foreground">
<Image src={siteLogo} alt={`Логотип ${SITE_NAME}`} width={20} height={20} className="rounded-sm" priority />
<span className="hidden truncate sm:inline">{SITE_NAME}</span>
</Link>

<nav className="flex items-center gap-1">
<header className="sticky top-0 z-50 border-b border-border/70 bg-background/95 backdrop-blur-xl">
<div className="mx-auto w-full max-w-[1440px] px-4 sm:px-6 lg:px-8">
<div className="grid min-h-[var(--site-header-height)] gap-1.5 py-3 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:items-center md:gap-6 md:py-0">
<div className="flex items-center justify-between md:contents">
<Link
href="/"
aria-label="Главная"
className="inline-flex h-9 items-center gap-1.5 rounded-md px-2 text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground"
>
<Home className="size-4" />
<span>Главная</span>
</Link>
<Link
href="/docs"
aria-label="Документация"
className="inline-flex h-9 items-center gap-1.5 rounded-md px-2 text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground"
className="inline-flex min-w-0 items-center gap-3 md:col-start-1 md:row-start-1 md:justify-self-start"
>
<BookOpenText className="size-4" />
<span>Документация</span>
<Image
src={siteLogo}
alt={`Логотип ${SITE_NAME}`}
width={48}
height={48}
className="h-10 w-10 rounded-sm md:h-12 md:w-12"
priority
/>
<span className="truncate text-xl font-medium tracking-tight text-foreground/60">{SITE_NAME}</span>
</Link>
<Link
href="/ask"
aria-label="Спроси StackMIREA"
className="inline-flex h-9 items-center gap-1.5 rounded-md px-2 text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground"
>
<BrainCircuit className="size-4" />
<span>Спросить</span>
</Link>
</nav>
</div>

<div className="flex items-center gap-1">
<ThemeToggle />
<Link
href={REPO_URL}
target="_blank"
rel="noreferrer"
className="inline-flex h-9 items-center gap-2 rounded-md px-2.5 text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground"
<div className="md:col-start-3 md:row-start-1 md:justify-self-end">
<ThemeToggle className="h-10 w-10 rounded-full text-foreground/55 hover:bg-muted/60 hover:text-foreground md:h-11 md:w-11" />
</div>
</div>

<nav
aria-label="Основная навигация"
className="flex items-center gap-1 overflow-x-auto pb-0.5 md:col-start-2 md:row-start-1 md:justify-self-center md:overflow-visible md:pb-0"
>
<Github className="size-4" />
<span className="hidden sm:inline">GitHub</span>
</Link>
{navigation.map((item) => {
const Icon = item.icon;
const isActive = item.href === "/" ? pathname === item.href : pathname.startsWith(item.href);

return (
<Link
key={item.href}
href={item.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"inline-flex h-9 shrink-0 items-center gap-2 rounded-full px-3.5 text-sm font-medium transition-colors md:h-10 md:px-4",
isActive
? "bg-[#eef3ff] text-[#5c8dff] dark:bg-white/5 dark:text-[#8cb0ff]"
: "text-muted-foreground hover:bg-muted/70 hover:text-foreground"
)}
>
<Icon className="size-4" strokeWidth={2.05} />
<span>{item.label}</span>
</Link>
);
})}
</nav>
</div>
</div>
</header>
Expand Down
2 changes: 1 addition & 1 deletion components/layout/MobileDocsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function MobileDocsMenu({ buildInfo, groups, currentPath }: MobileDocsMen
}

return (
<div className="sticky top-14 z-30 border-b border-border/80 bg-background/95 px-4 py-3 backdrop-blur md:hidden">
<div className="sticky top-[var(--site-header-height)] z-30 border-b border-border/80 bg-background/95 px-4 py-3 backdrop-blur md:hidden">
<button
className="flex items-center gap-1 py-1 text-sm font-medium text-foreground"
type="button"
Expand Down
17 changes: 14 additions & 3 deletions components/search/AskStackMirea.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { ArrowRight, BookOpenText, BrainCircuit, Loader2, Search, Sparkles } from "lucide-react";
import { type FormEvent, startTransition, useDeferredValue, useState } from "react";
import { type FormEvent, startTransition, useDeferredValue, useEffect, useState } from "react";

import { Button, buttonVariants } from "@/components/ui/button";
import { useSearchIndex } from "@/components/search/useSearchIndex";
Expand All @@ -14,8 +15,13 @@ const allTopicDefinitions = getTopicDefinitions();
const popularTopicDefinitions = allTopicDefinitions.slice(0, 8);
const topicDefinitionsById = new Map(allTopicDefinitions.map((topic) => [topic.id, topic]));

export function AskStackMirea() {
const [query, setQuery] = useState("");
interface AskStackMireaProps {
initialQuery?: string;
}

export function AskStackMirea({ initialQuery = "" }: AskStackMireaProps) {
const searchParams = useSearchParams();
const [query, setQuery] = useState(initialQuery);
const { index, error, isLoading } = useSearchIndex();
const deferredQuery = useDeferredValue(query);

Expand All @@ -35,6 +41,11 @@ export function AskStackMirea() {
applyQuery(query.trim());
}

useEffect(() => {
const queryFromUrl = searchParams.get("q");
setQuery(queryFromUrl?.trim() || initialQuery);
}, [initialQuery, searchParams]);

return (
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_320px]">
<section className="rounded-3xl border border-border/70 bg-card/70 p-6 sm:p-7">
Expand Down
Loading
Loading