mirror of
https://github.com/AderKonstantin/aderktech-chronark.com-.git
synced 2025-06-08 05:38:41 +03:00
Update .dockerignore, .env, route.ts, and 8 more files
This commit is contained in:
parent
ed71683810
commit
65db212838
@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
.git
|
||||
.next
|
||||
.vscode
|
||||
|
||||
Dockerfile
|
||||
.dockerignore
|
4
.env
4
.env
@ -1,3 +1,3 @@
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_HOST="172.18.0.2"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_PASSWORD= # если используется
|
@ -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 });
|
||||
}
|
||||
}
|
@ -19,17 +19,6 @@ export const metadata: Metadata = {
|
||||
locale: "ru-RU",
|
||||
type: "website",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
shortcut: "/favicon.png",
|
||||
},
|
||||
|
@ -10,9 +10,8 @@ type Props = {
|
||||
description: 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 [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="flex justify-between gap-8">
|
||||
<span
|
||||
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">
|
||||
<Link target="_blank" href="https://github.com/AderKonstantin">
|
||||
<Github
|
||||
className={`w-6 h-6 duration-200 hover:font-medium ${
|
||||
isIntersecting
|
||||
|
@ -4,7 +4,6 @@ import { Mdx } from "@/app/components/mdx";
|
||||
import { Header } from "./header";
|
||||
import "./mdx.css";
|
||||
import { ReportView } from "./view";
|
||||
import Redis from "ioredis";
|
||||
|
||||
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"][]> {
|
||||
return allProjects
|
||||
.filter((p) => p.published)
|
||||
@ -37,15 +29,9 @@ export default async function PostPage({ params }: Props) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Получаем и преобразуем значение просмотров
|
||||
const views = parseInt(
|
||||
(await redis.get(`projects:${slug}:views`)) || "0",
|
||||
10
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-zinc-50 min-h-screen">
|
||||
<Header project={project} views={views} />
|
||||
<Header project={project} />
|
||||
<ReportView slug={project.slug} />
|
||||
|
||||
<article className="px-4 py-12 mx-auto prose prose-zinc prose-quoteless">
|
||||
|
@ -4,10 +4,9 @@ import { Eye, View } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
project: Project;
|
||||
views: number;
|
||||
};
|
||||
|
||||
export const Article: React.FC<Props> = ({ project, views }) => {
|
||||
export const Article: React.FC<Props> = ({ project }) => {
|
||||
return (
|
||||
<Link href={`/projects/${project.slug}`}>
|
||||
<article className="p-4 md:p-8">
|
||||
@ -23,10 +22,6 @@ export const Article: React.FC<Props> = ({ project, views }) => {
|
||||
<span>SOON</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>
|
||||
<h2 className="z-20 text-xl font-medium duration-1000 lg:text-3xl text-zinc-200 group-hover:text-white font-display">
|
||||
{project.title}
|
||||
|
@ -8,30 +8,8 @@ import { Card } from "../components/card";
|
||||
import { Article } from "./article";
|
||||
import { Eye } from "lucide-react";
|
||||
|
||||
type ViewsData = Record<string, number>;
|
||||
|
||||
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 top2 = allProjects.find((project) => project.slug === "blog")!;
|
||||
const top3 = allProjects.find((project) => project.slug === "bimkaspace")!;
|
||||
@ -80,12 +58,6 @@ export default function ProjectsPage() {
|
||||
<span>SOON</span>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<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 ">
|
||||
{[top2, top3].map((project) => (
|
||||
<Card key={project.slug}>
|
||||
<Article project={project} views={views[project.slug] ?? 0} />
|
||||
<Article project={project} />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@ -122,7 +94,7 @@ export default function ProjectsPage() {
|
||||
.filter((_, i) => i % 3 === 0)
|
||||
.map((project) => (
|
||||
<Card key={project.slug}>
|
||||
<Article project={project} views={views[project.slug] ?? 0} />
|
||||
<Article project={project} />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@ -131,7 +103,7 @@ export default function ProjectsPage() {
|
||||
.filter((_, i) => i % 3 === 1)
|
||||
.map((project) => (
|
||||
<Card key={project.slug}>
|
||||
<Article project={project} views={views[project.slug] ?? 0} />
|
||||
<Article project={project} />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@ -140,7 +112,7 @@ export default function ProjectsPage() {
|
||||
.filter((_, i) => i % 3 === 2)
|
||||
.map((project) => (
|
||||
<Card key={project.slug}>
|
||||
<Article project={project} views={views[project.slug] ?? 0} />
|
||||
<Article project={project} />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,47 +1,12 @@
|
||||
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:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: main-aderk-next
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.next.rule=Host(`aderk.tech`)"
|
||||
- "traefik.http.routers.next.entrypoints=https"
|
||||
- "traefik.http.routers.next.tls=true"
|
||||
- "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
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
# - "traefik.http.routers.next.rule=Host(`aderk.tech`)"
|
||||
# - "traefik.http.routers.next.entrypoints=https"
|
||||
# - "traefik.http.routers.next.tls=true"
|
||||
# - "traefik.http.services.next.loadbalancer.server.port=3000"
|
||||
|
18
lib/redis.ts
18
lib/redis.ts
@ -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;
|
||||
};
|
@ -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 });
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user