Update .dockerignore, .env, route.ts, and 8 more files

This commit is contained in:
AderKonstantin 2025-05-19 12:25:05 +03:00
parent ed71683810
commit 65db212838
11 changed files with 16 additions and 235 deletions

View File

@ -1,7 +0,0 @@
node_modules
.git
.next
.vscode
Dockerfile
.dockerignore

4
.env
View File

@ -1,3 +1,3 @@
REDIS_HOST=localhost REDIS_HOST="172.18.0.2"
REDIS_PORT=6379 REDIS_PORT="6379"
REDIS_PASSWORD= # если используется REDIS_PASSWORD= # если используется

View File

@ -1,25 +0,0 @@
import { NextResponse } from "next/server";
import { getRedisClient } from "../../../lib/redis";
export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';
export async function GET() {
try {
const redis = getRedisClient();
const keys = await redis.keys('projects:*:views');
if (keys.length === 0) return NextResponse.json({});
const values = await redis.mget(...keys);
const views = keys.reduce((acc, key, i) => {
const slug = key.split(':')[1];
acc[slug] = parseInt(values[i] as string) || 0;
return acc;
}, {} as Record<string, number>);
return NextResponse.json(views);
} catch (error) {
console.error('Redis error:', error);
return NextResponse.json({}, { status: 500 });
}
}

View File

@ -19,17 +19,6 @@ export const metadata: Metadata = {
locale: "ru-RU", locale: "ru-RU",
type: "website", type: "website",
}, },
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: { icons: {
shortcut: "/favicon.png", shortcut: "/favicon.png",
}, },

View File

@ -10,9 +10,8 @@ type Props = {
description: string; description: string;
repository?: string; repository?: string;
}; };
views: number;
}; };
export const Header: React.FC<Props> = ({ project, views }) => { export const Header: React.FC<Props> = ({ project}) => {
const ref = useRef<HTMLElement>(null); const ref = useRef<HTMLElement>(null);
const [isIntersecting, setIntersecting] = useState(true); const [isIntersecting, setIntersecting] = useState(true);
@ -53,29 +52,7 @@ export const Header: React.FC<Props> = ({ project, views }) => {
> >
<div className="container flex flex-row-reverse items-center justify-between p-6 mx-auto"> <div className="container flex flex-row-reverse items-center justify-between p-6 mx-auto">
<div className="flex justify-between gap-8"> <div className="flex justify-between gap-8">
<span <Link target="_blank" href="https://github.com/AderKonstantin">
title="View counter for this page"
className={`duration-200 hover:font-medium flex items-center gap-1 ${
isIntersecting
? " text-zinc-400 hover:text-zinc-100"
: "text-zinc-600 hover:text-zinc-900"
} `}
>
<Eye className="w-5 h-5" />{" "}
{Intl.NumberFormat("en-US", { notation: "compact" }).format(
views,
)}
</span>
<Link target="_blank" href="https://twitter.com/chronark_">
<Twitter
className={`w-6 h-6 duration-200 hover:font-medium ${
isIntersecting
? " text-zinc-400 hover:text-zinc-100"
: "text-zinc-600 hover:text-zinc-900"
} `}
/>
</Link>
<Link target="_blank" href="https://github.com/chronark">
<Github <Github
className={`w-6 h-6 duration-200 hover:font-medium ${ className={`w-6 h-6 duration-200 hover:font-medium ${
isIntersecting isIntersecting

View File

@ -4,7 +4,6 @@ import { Mdx } from "@/app/components/mdx";
import { Header } from "./header"; import { Header } from "./header";
import "./mdx.css"; import "./mdx.css";
import { ReportView } from "./view"; import { ReportView } from "./view";
import Redis from "ioredis";
export const revalidate = 60; export const revalidate = 60;
@ -14,13 +13,6 @@ type Props = {
}; };
}; };
// Настройка подключения к локальному Redis
const redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379"),
password: process.env.REDIS_PASSWORD,
});
export async function generateStaticParams(): Promise<Props["params"][]> { export async function generateStaticParams(): Promise<Props["params"][]> {
return allProjects return allProjects
.filter((p) => p.published) .filter((p) => p.published)
@ -37,15 +29,9 @@ export default async function PostPage({ params }: Props) {
notFound(); notFound();
} }
// Получаем и преобразуем значение просмотров
const views = parseInt(
(await redis.get(`projects:${slug}:views`)) || "0",
10
);
return ( return (
<div className="bg-zinc-50 min-h-screen"> <div className="bg-zinc-50 min-h-screen">
<Header project={project} views={views} /> <Header project={project} />
<ReportView slug={project.slug} /> <ReportView slug={project.slug} />
<article className="px-4 py-12 mx-auto prose prose-zinc prose-quoteless"> <article className="px-4 py-12 mx-auto prose prose-zinc prose-quoteless">

View File

@ -4,10 +4,9 @@ import { Eye, View } from "lucide-react";
type Props = { type Props = {
project: Project; project: Project;
views: number;
}; };
export const Article: React.FC<Props> = ({ project, views }) => { export const Article: React.FC<Props> = ({ project }) => {
return ( return (
<Link href={`/projects/${project.slug}`}> <Link href={`/projects/${project.slug}`}>
<article className="p-4 md:p-8"> <article className="p-4 md:p-8">
@ -23,10 +22,6 @@ export const Article: React.FC<Props> = ({ project, views }) => {
<span>SOON</span> <span>SOON</span>
)} )}
</span> </span>
<span className="text-zinc-500 text-xs flex items-center gap-1">
<Eye className="w-4 h-4" />{" "}
{Intl.NumberFormat("en-US", { notation: "compact" }).format(views)}
</span>
</div> </div>
<h2 className="z-20 text-xl font-medium duration-1000 lg:text-3xl text-zinc-200 group-hover:text-white font-display"> <h2 className="z-20 text-xl font-medium duration-1000 lg:text-3xl text-zinc-200 group-hover:text-white font-display">
{project.title} {project.title}

View File

@ -8,30 +8,8 @@ import { Card } from "../components/card";
import { Article } from "./article"; import { Article } from "./article";
import { Eye } from "lucide-react"; import { Eye } from "lucide-react";
type ViewsData = Record<string, number>;
export default function ProjectsPage() { export default function ProjectsPage() {
const [views, setViews] = useState<ViewsData>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchViews = async () => {
try {
const response = await fetch('/api/views');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
setViews(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchViews();
}, []);
const featured = allProjects.find((project) => project.slug === "cbg")!; const featured = allProjects.find((project) => project.slug === "cbg")!;
const top2 = allProjects.find((project) => project.slug === "blog")!; const top2 = allProjects.find((project) => project.slug === "blog")!;
const top3 = allProjects.find((project) => project.slug === "bimkaspace")!; const top3 = allProjects.find((project) => project.slug === "bimkaspace")!;
@ -80,12 +58,6 @@ export default function ProjectsPage() {
<span>SOON</span> <span>SOON</span>
)} )}
</div> </div>
<span className="flex items-center gap-1 text-xs text-zinc-500">
<Eye className="w-4 h-4" />{" "}
{Intl.NumberFormat("en-US", { notation: "compact" }).format(
views[featured.slug] ?? 0,
)}
</span>
</div> </div>
<h2 <h2
@ -109,7 +81,7 @@ export default function ProjectsPage() {
<div className="flex flex-col w-full gap-8 mx-auto border-t border-gray-900/10 lg:mx-0 lg:border-t-0 "> <div className="flex flex-col w-full gap-8 mx-auto border-t border-gray-900/10 lg:mx-0 lg:border-t-0 ">
{[top2, top3].map((project) => ( {[top2, top3].map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} />
</Card> </Card>
))} ))}
</div> </div>
@ -122,7 +94,7 @@ export default function ProjectsPage() {
.filter((_, i) => i % 3 === 0) .filter((_, i) => i % 3 === 0)
.map((project) => ( .map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} />
</Card> </Card>
))} ))}
</div> </div>
@ -131,7 +103,7 @@ export default function ProjectsPage() {
.filter((_, i) => i % 3 === 1) .filter((_, i) => i % 3 === 1)
.map((project) => ( .map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} />
</Card> </Card>
))} ))}
</div> </div>
@ -140,7 +112,7 @@ export default function ProjectsPage() {
.filter((_, i) => i % 3 === 2) .filter((_, i) => i % 3 === 2)
.map((project) => ( .map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} />
</Card> </Card>
))} ))}
</div> </div>

View File

@ -1,47 +1,12 @@
services: services:
redis:
image: redis:alpine
container_name: main-aderk-redis
volumes:
- redis_data:/data
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
networks:
- backend
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
timeout: 3s
retries: 3
next: next:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: main-aderk-next container_name: main-aderk-next
labels: # labels:
- "traefik.enable=true" # - "traefik.enable=true"
- "traefik.http.routers.next.rule=Host(`aderk.tech`)" # - "traefik.http.routers.next.rule=Host(`aderk.tech`)"
- "traefik.http.routers.next.entrypoints=https" # - "traefik.http.routers.next.entrypoints=https"
- "traefik.http.routers.next.tls=true" # - "traefik.http.routers.next.tls=true"
- "traefik.http.services.next.loadbalancer.server.port=3000" # - "traefik.http.services.next.loadbalancer.server.port=3000"
environment:
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- REDIS_PASSWORD=${REDIS_PASSWORD}
depends_on:
- redis
networks:
- proxy
- backend
volumes:
redis_data:
networks:
proxy:
external: true
backend:
internal: true

View File

@ -1,18 +0,0 @@
import Redis from "ioredis";
let redis: Redis | null = null;
export const getRedisClient = () => {
if (!redis) {
redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379"),
password: process.env.REDIS_PASSWORD,
});
redis.on("error", (err) => {
console.error("Redis error:", err);
});
}
return redis;
};

View File

@ -1,53 +0,0 @@
import Redis from "ioredis";
import { NextRequest, NextResponse } from "next/server";
// Настройка подключения к локальному Redis
const redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379"),
password: process.env.REDIS_PASSWORD,
});
export const config = {
runtime: "nodejs", // Возможно потребуется изменить на "nodejs" если возникнут проблемы
};
export default async function incr(req: NextRequest): Promise<NextResponse> {
if (req.method !== "POST") {
return new NextResponse("use POST", { status: 405 });
}
if (req.headers.get("Content-Type") !== "application/json") {
return new NextResponse("must be json", { status: 400 });
}
const body = await req.json();
const slug = body.slug;
if (!slug) {
return new NextResponse("Slug not found", { status: 400 });
}
const ip = req.ip;
if (ip) {
// Хеширование IP-адреса
const buf = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(ip)
);
const hash = Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// Проверка уникальности посещения
const key = `deduplicate:${hash}:${slug}`;
const isNew = await redis.set(key, "1", "EX", 86400, "NX");
if (!isNew) {
return new NextResponse(null, { status: 202 });
}
}
// Увеличиваем счетчик просмотров
await redis.incr(`projects:${slug}:views`);
return new NextResponse(null, { status: 202 });
}