Add Django backend, Next.js frontend, and deploy templates
This commit is contained in:
1 parent
fc0c991335
commit
e9cd22d36f
93 files changed
+7840
-1
No files matched your search
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Download, Eye, FileText } from "lucide-react";
|
||||
|
||||
type DocumentItem = {
|
||||
id: number;
|
||||
title: string;
|
||||
file?: string;
|
||||
mode: "preview" | "download" | "disabled";
|
||||
};
|
||||
|
||||
type Props = {
|
||||
items: DocumentItem[];
|
||||
};
|
||||
|
||||
function getPreviewUrl(origin: string, file: string) {
|
||||
const absoluteUrl = `${origin}/documents/${file}`;
|
||||
const extension = file.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (extension === "pdf") {
|
||||
return `${absoluteUrl}#toolbar=0&navpanes=0&scrollbar=1`;
|
||||
}
|
||||
|
||||
return `https://docs.google.com/gview?embedded=1&url=${encodeURIComponent(absoluteUrl)}`;
|
||||
}
|
||||
|
||||
export function DocumentsViewer({ items }: Props) {
|
||||
const firstPreviewable = items.find((item) => item.mode === "preview" && item.file) ?? null;
|
||||
const [selectedId, setSelectedId] = useState<number>(firstPreviewable?.id ?? 0);
|
||||
const [origin, setOrigin] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setOrigin(window.location.origin);
|
||||
}, []);
|
||||
|
||||
const selectedItem =
|
||||
items.find((item) => item.id === selectedId && item.mode === "preview" && item.file) ??
|
||||
firstPreviewable;
|
||||
|
||||
return (
|
||||
<div className="documents-layout">
|
||||
<aside className="documents-sidebar">
|
||||
<div className="documents-sidebar__panel">
|
||||
{items.map((item) => {
|
||||
if (item.mode === "download" && item.file) {
|
||||
return (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`/documents/${item.file}`}
|
||||
download
|
||||
className="documents-link documents-link--download"
|
||||
>
|
||||
<Download size={16} />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.mode === "disabled") {
|
||||
return (
|
||||
<div key={item.id} className="documents-link documents-link--disabled">
|
||||
<FileText size={16} />
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedId(item.id)}
|
||||
className={`documents-link documents-link--button${
|
||||
selectedItem?.id === item.id ? " documents-link--active" : ""
|
||||
}`}
|
||||
>
|
||||
<Eye size={16} />
|
||||
<span>{item.title}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="documents-content">
|
||||
<article className="page-panel documents-preview">
|
||||
<div className="documents-preview__header">
|
||||
<div>
|
||||
<span className="eyebrow">Просмотр документа</span>
|
||||
<h2>{selectedItem?.title ?? "Документ не выбран"}</h2>
|
||||
</div>
|
||||
<div className="documents-preview__note">
|
||||
<FileText size={18} />
|
||||
<span>Для просмотра используйте список слева</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedItem?.file && origin ? (
|
||||
<div className="documents-preview__frame">
|
||||
<iframe
|
||||
key={`${origin}-${selectedItem.file}`}
|
||||
src={getPreviewUrl(origin, selectedItem.file)}
|
||||
title={selectedItem.title}
|
||||
className="documents-preview__iframe"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="documents-preview__empty">
|
||||
Документ загрузится после открытия страницы в браузере.
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FileText, Microscope, ShieldPlus, Stethoscope } from "lucide-react";
|
||||
|
||||
export function resolveIcon(name: string) {
|
||||
const props = { size: 22, strokeWidth: 2 };
|
||||
|
||||
switch (name) {
|
||||
case "microscope":
|
||||
return <Microscope {...props} />;
|
||||
case "stethoscope":
|
||||
return <Stethoscope {...props} />;
|
||||
case "file-text":
|
||||
return <FileText {...props} />;
|
||||
default:
|
||||
return <ShieldPlus {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, MapPinned, Phone } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
siteName: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
workingHours: string;
|
||||
};
|
||||
|
||||
const footerLinks = [
|
||||
{
|
||||
title: "Госуслуги",
|
||||
href: "https://www.gosuslugi.ru/",
|
||||
icon: "https://www.google.com/s2/favicons?domain=www.gosuslugi.ru&sz=64",
|
||||
},
|
||||
{
|
||||
title: "НМО Минздрава РФ",
|
||||
href: "https://edu.rosminzdrav.ru/",
|
||||
icon: "https://www.google.com/s2/favicons?domain=edu.rosminzdrav.ru&sz=64",
|
||||
},
|
||||
{
|
||||
title: "Правительство СК",
|
||||
href: "https://www.stavregion.ru/",
|
||||
icon: "https://www.google.com/s2/favicons?domain=www.stavregion.ru&sz=64",
|
||||
},
|
||||
{
|
||||
title: "ТФОМС",
|
||||
href: "https://tfomssk.ru/",
|
||||
icon: "https://www.google.com/s2/favicons?domain=spboms.ru&sz=64",
|
||||
},
|
||||
];
|
||||
|
||||
const yandexMapsHref =
|
||||
"https://yandex.ru/maps/?text=%D0%A1%D1%82%D0%B0%D0%B2%D1%80%D0%BE%D0%BF%D0%BE%D0%BB%D1%8C%2C%20%D1%83%D0%BB%D0%B8%D1%86%D0%B0%20%D0%91%D0%B0%D0%BB%D0%B0%D0%BA%D0%B8%D1%80%D0%B5%D0%B2%D0%B0%2C%205%D0%BA7";
|
||||
|
||||
export function SiteFooter({ siteName, phone, email, address, workingHours }: Props) {
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="container">
|
||||
<div className="footer-panel">
|
||||
<div className="footer-tiles">
|
||||
{footerLinks.map((item) => (
|
||||
<Link
|
||||
key={item.title}
|
||||
href={item.href}
|
||||
className="footer-tile"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<span className="footer-tile__main">
|
||||
<span className="footer-tile__icon">
|
||||
<img src={item.icon} alt="" width="20" height="20" loading="lazy" />
|
||||
</span>
|
||||
<span>{item.title}</span>
|
||||
</span>
|
||||
<ExternalLink size={14} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="footer-bottom">
|
||||
<Link
|
||||
href={yandexMapsHref}
|
||||
className="footer-map"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<div className="footer-map__badge">
|
||||
<MapPinned size={16} />
|
||||
<span>Как нас найти</span>
|
||||
</div>
|
||||
<div className="footer-map__title">{address}</div>
|
||||
<div className="footer-map__meta">{workingHours}</div>
|
||||
</Link>
|
||||
|
||||
<div className="footer-contact">
|
||||
<strong>{siteName}</strong>
|
||||
<div className="footer-meta">{address}</div>
|
||||
<div className="footer-meta">{workingHours}</div>
|
||||
<a className="footer-contact__phone" href={`tel:${phone.replace(/[^\d+]/g, "")}`}>
|
||||
<Phone size={16} />
|
||||
<span>{phone}</span>
|
||||
</a>
|
||||
<div className="footer-meta">{email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type Props = {
|
||||
siteName: string;
|
||||
tagline: string;
|
||||
phone: string;
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ label: "Главная", href: "/" },
|
||||
{ label: "Услуги", href: "/services" },
|
||||
{ label: "Специалисты", href: "/specialists" },
|
||||
{ label: "Документы", href: "/documents" },
|
||||
{ label: "Отзывы", href: "/reviews" },
|
||||
];
|
||||
|
||||
export function SiteHeader({ siteName, tagline, phone }: Props) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const compactName = siteName
|
||||
.replace("Консультационное клиническое ", "")
|
||||
.replace("консультационное клиническое ", "");
|
||||
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="container site-header__inner">
|
||||
<Link href="/" className="brand">
|
||||
<span className="brand__eyebrow">Ставрополь</span>
|
||||
<span className="brand__name">{compactName || "Патологоанатомическое бюро"}</span>
|
||||
<span className="brand__tag">{tagline}</span>
|
||||
</Link>
|
||||
|
||||
<nav className="nav" aria-label="Основная навигация">
|
||||
{navItems.map((item) => {
|
||||
const isActive =
|
||||
item.href === "/"
|
||||
? pathname === "/"
|
||||
: pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`nav__link${isActive ? " nav__link--active" : ""}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="header-actions">
|
||||
<a className="phone-pill" href={`tel:${phone.replace(/[^\d+]/g, "")}`}>
|
||||
{phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user