frontend #1

Merged
d3m0k1d merged 13 commits from frontend into backend 2026-04-03 22:47:30 +00:00
9 changed files with 376 additions and 0 deletions
Showing only changes of commit cc23cc2a1e - Show all commits
@@ -1,6 +1,7 @@
import { Suspense } from "react";
import { Routes as ReactRoutes, Route, Navigate } from "react-router-dom";
import { HomePage } from "@/pages/home.page";
import { ThemesPage } from "@/pages/themes.page";
export const Routing = () => {
return (
@@ -13,6 +14,7 @@ export const Routing = () => {
>
<ReactRoutes>
<Route path="/" element={<HomePage />} />
<Route path="/themes" element={<ThemesPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</ReactRoutes>
@@ -0,0 +1,106 @@
export const themes = [
{
id: "light",
name: "Светлая",
description: "Чистая светлая тема",
type: "light",
colors: {
primary: "#4f46e5",
background: "#ffffff",
surface: "#f8fafc",
text: "#1f2937",
border: "#e5e7eb",
},
},
{
id: "dark",
name: "Темная",
description: "Элегантная темная тема",
type: "dark",
colors: {
primary: "#6366f1",
background: "#0f172a",
surface: "#1e293b",
text: "#f1f5f9",
border: "#334155",
},
},
{
id: "nightowl",
name: "Night Owl",
description: "Тема вдохновленная редактором кода",
type: "dark",
colors: {
primary: "#7dd3fc",
background: "#011627",
surface: "#011e3c",
text: "#d6deeb",
border: "#1d3b53",
},
},
{
id: "sunset",
name: "Закат",
description: "Теплые оранжевые тона",
type: "dark",
colors: {
primary: "#f97316",
background: "#1c1917",
surface: "#292524",
text: "#fafaf9",
border: "#57534e",
},
},
{
id: "forest",
name: "Лес",
description: "Успокаивающая зеленая тема",
type: "dark",
colors: {
primary: "#22c55e",
background: "#052e16",
surface: "#14532d",
text: "#f0fdf4",
border: "#166534",
},
},
{
id: "ocean",
name: "Океан",
description: "Глубокие синие тона",
type: "dark",
colors: {
primary: "#06b6d4",
background: "#164e63",
surface: "#0e7490",
text: "#f0fdfd",
border: "#0891b2",
},
},
{
id: "lavender",
name: "Лаванда",
description: "Нежная фиолетовая тема",
type: "light",
colors: {
primary: "#a855f7",
background: "#faf5ff",
surface: "#f3e8ff",
text: "#581c87",
border: "#e9d5ff",
},
},
{
id: "coffee",
name: "Кофе",
description: "Уютная коричневая тема",
type: "dark",
colors: {
primary: "#d97706",
background: "#292524",
surface: "#44403c",
text: "#f5f5f4",
border: "#57534e",
},
},
];
@@ -0,0 +1,2 @@
export { ThemeInitialProvider } from "./provider/theme.initial.provider";
export { ThemeChanger } from "./ui/theme.changer";
@@ -0,0 +1,13 @@
import { useLayoutEffect } from "react";
import { applyTheme, initializeTheme } from "../utils/apply.theme";
export const ThemeInitialProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
useLayoutEffect(() => {
const theme = initializeTheme();
applyTheme(theme);
}, []);
return children;
};
@@ -0,0 +1,13 @@
export interface ITheme {
id: string;
name: string;
description: string;
type: string;
colors: {
primary: string;
background: string;
surface: string;
text: string;
border: string;
};
}
@@ -0,0 +1,78 @@
import type { ITheme } from "../../types/theme.type";
interface IProps {
theme: ITheme;
isSelected: boolean;
onSelect: (id: string) => void;
}
export const ThemeCard: React.FC<IProps> = ({
theme,
isSelected,
onSelect,
}) => {
const { id, name, description } = theme;
return (
<button
onClick={() => onSelect(id)}
className={`relative p-4 rounded-xl border-2 transition-all duration-200 hover:scale-105 hover:shadow-large w-full text-left ${
isSelected
? "border-accent ring-2 ring-accent ring-opacity-50"
: "border-primary hover:border-secondary"
} bg-secondary`}
>
<div
className={`absolute -top-2 -right-2 w-6 h-6 rounded-full flex items-center justify-center transition-all ${
isSelected
? "scale-100 opacity-100 accent-primary"
: "scale-0 opacity-0"
}`}
>
<svg
className="w-3 h-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="space-y-3">
<div
className="h-20 rounded-lg border-2 p-2 space-y-1 bg-primary border-primary"
data-theme={id}
>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full accent-primary" />
<div className="h-1 flex-1 rounded bg-secondary" />
</div>
<div className="space-y-1">
<div
className="h-1 rounded accent-primary"
style={{ width: "70%" }}
/>
<div
className="h-1 rounded bg-secondary"
style={{ width: "40%" }}
/>
<div
className="h-1 rounded bg-secondary"
style={{ width: "60%" }}
/>
</div>
</div>
<div className="space-y-1">
<h3 className="font-semibold text-sm text-primary">{name}</h3>
<p className="text-xs text-secondary">{description}</p>
</div>
</div>
</button>
);
};
@@ -0,0 +1,48 @@
import type { ITheme } from "../types/theme.type";
import { applyTheme } from "../utils/apply.theme";
import { ThemeCard } from "./components/theme.card";
import { themes as baseThemes } from "../config/theme.config";
interface IProps {
themes?: ITheme[];
label: string;
currentTheme?: string;
setTheme?: (id: string) => void;
}
export const ThemeChanger: React.FC<IProps> = ({
themes = baseThemes,
label,
currentTheme,
setTheme,
}) => {
const onSelectTheme = (theme: string) => {
applyTheme(theme);
setTheme?.(theme);
};
return (
<div className="">
<h4 className="text-sm font-medium text-secondary mb-3 flex items-center gap-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
{label}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{themes.map((theme) => (
<ThemeCard
key={theme.id}
theme={theme}
isSelected={currentTheme === theme.id}
onSelect={onSelectTheme}
/>
))}
</div>
</div>
);
};
@@ -0,0 +1,105 @@
import { themes } from "../config/theme.config";
export const applyTheme = (themeId: string) => {
const theme = themes.find((t) => t.id === themeId);
const root = document.documentElement;
if (theme) {
try {
root.setAttribute("data-theme", themeId);
localStorage.setItem("theme", themeId);
localStorage.setItem("theme-type", theme.type);
window.dispatchEvent(
new CustomEvent("themechange", {
detail: { theme: themeId, type: theme.type },
}),
);
} catch (error) {
console.error("❌ Error applying theme:", error);
}
} else {
console.warn(`⚠️ Theme not found: ${themeId}, falling back to light theme`);
applyTheme("light");
}
};
export const getSavedTheme = () => {
try {
return localStorage.getItem("theme") || "light";
} catch (error) {
console.error("Error reading theme from localStorage:", error);
return "light";
}
};
export const initializeTheme = () => {
const savedTheme = getSavedTheme();
const themeExists = themes.some((t) => t.id === savedTheme);
const themeToApply = themeExists ? savedTheme : "light";
applyTheme(themeToApply);
return themeToApply;
};
export const getCurrentTheme = () => {
return document.documentElement.getAttribute("data-theme") || "light";
};
export const getCurrentThemeType = () => {
const currentTheme = getCurrentTheme();
const theme = themes.find((t) => t.id === currentTheme);
return theme ? theme.type : "light";
};
export const toggleDarkLight = () => {
const currentTheme = getCurrentTheme();
const currentThemeData = themes.find((t) => t.id === currentTheme);
if (currentThemeData) {
const oppositeThemes = themes.filter(
(t) => t.type !== currentThemeData.type,
);
if (oppositeThemes.length > 0) {
applyTheme(oppositeThemes[0].id);
return oppositeThemes[0].id;
}
}
const newTheme = currentTheme === "light" ? "dark" : "light";
applyTheme(newTheme);
return newTheme;
};
export const getNextTheme = () => {
const currentTheme = getCurrentTheme();
const currentIndex = themes.findIndex((t) => t.id === currentTheme);
const nextIndex = (currentIndex + 1) % themes.length;
return themes[nextIndex].id;
};
export const applySystemTheme = () => {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
applyTheme("dark");
} else {
applyTheme("light");
}
};
export const watchSystemTheme = () => {
if (window.matchMedia) {
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (e.matches) {
applyTheme("dark");
} else {
applyTheme("light");
}
});
}
};
+9
View File
@@ -0,0 +1,9 @@
import { ThemeChanger } from "@/modules/theme-changer";
export const ThemesPage = () => {
return (
<div>
<ThemeChanger label="Выбор тем приложения" />
</div>
);
};