Compare commits

..

4 Commits

Author SHA1 Message Date
AderKonstantin
ed71683810 Update layout.tsx, favicon.jpg, and favicon.png 2025-05-18 22:56:31 +03:00
AderKonstantin
39edf4ea49 Update route.ts and page.tsx 2025-05-18 16:55:41 +03:00
AderKonstantin
347a0c638c Update route.ts and docker-compose.yml 2025-05-18 16:28:59 +03:00
AderKonstantin
9bd8773b3b Update .dockerignore, route.ts, layout.tsx, and 6 more files 2025-05-18 16:22:03 +03:00
11 changed files with 105 additions and 52 deletions

7
.dockerignore Normal file
View File

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

25
app/api/views/route.ts Normal file
View File

@ -0,0 +1,25 @@
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

@ -16,13 +16,6 @@ export const metadata: Metadata = {
"Junior Dev & Student", "Junior Dev & Student",
url: "https://aderk.tech", url: "https://aderk.tech",
siteName: "aderk.tech", siteName: "aderk.tech",
images: [
{
url: "https://aderk.tech/og.png",
width: 1920,
height: 1080,
},
],
locale: "ru-RU", locale: "ru-RU",
type: "website", type: "website",
}, },
@ -38,7 +31,7 @@ export const metadata: Metadata = {
}, },
}, },
icons: { icons: {
shortcut: "/favicon.jpg", shortcut: "/favicon.png",
}, },
}; };
const inter = Inter({ const inter = Inter({

View File

@ -10,7 +10,6 @@ type Props = {
description: string; description: string;
repository?: string; repository?: string;
}; };
views: number; views: number;
}; };
export const Header: React.FC<Props> = ({ project, views }) => { export const Header: React.FC<Props> = ({ project, views }) => {

View File

@ -1,33 +1,41 @@
'use client';
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React, { useEffect, useState } from "react";
import { allProjects } from "contentlayer/generated"; import { allProjects } from "contentlayer/generated";
import { Navigation } from "../components/nav"; import { Navigation } from "../components/nav";
import { Card } from "../components/card"; import { Card } from "../components/card";
import { Article } from "./article"; import { Article } from "./article";
import Redis from "ioredis"; // Заменяем @upstash/redis на ioredis
import { Eye } from "lucide-react"; import { Eye } from "lucide-react";
// Создаем подключение к локальному Redis type ViewsData = Record<string, number>;
const redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379"),
password: process.env.REDIS_PASSWORD, // если есть пароль
});
export const revalidate = 60; export default function ProjectsPage() {
export default async function ProjectsPage() { const [views, setViews] = useState<ViewsData>({});
const viewsEntries = await redis.mget( const [loading, setLoading] = useState(true);
...allProjects.map((p) => `projects:${p.slug}:views`) const [error, setError] = useState<string | null>(null);
);
const views = allProjects.reduce((acc, project, index) => { useEffect(() => {
acc[project.slug] = parseInt(viewsEntries[index] as string) || 0; const fetchViews = async () => {
return acc; try {
}, {} as Record<string, number>); 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")!;
const sorted = allProjects const sorted = allProjects
.filter((p) => p.published) .filter((p) => p.published)
.filter( .filter(

View File

@ -20,4 +20,5 @@ published: true
- [Gin](https://gin-gonic.com/) - [Gin](https://gin-gonic.com/)
### DataBase ### DataBase
- PostgreSQL - PostgreSQL

View File

@ -1,45 +1,47 @@
services: services:
next:
build:
context: .
dockerfile: Dockerfile
container_name: example-frontend
labels:
- "traefik.enable=true"
- "traefik.http.routers.next.rule=Host(`beta.example`)"
- "traefik.http.routers.next.entrypoints=https" # Thats correct
- "traefik.http.routers.next.tls=true" # I dont need certresolver here
- "traefik.http.services.next.loadbalancer.server.port=3000"
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
depends_on:
- redis
networks:
- proxy
- backend
redis: redis:
image: redis:alpine image: redis:alpine
container_name: main-aderk-redis
volumes: volumes:
- redis_data:/data - redis_data:/data
environment: environment:
- REDIS_PASSWORD=${REDIS_PASSWORD} - REDIS_PASSWORD=${REDIS_PASSWORD}
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
networks: networks:
- backend - backend
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: [ "CMD", "redis-cli", "ping" ]
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 3 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: volumes:
redis_data: redis_data:
networks: networks:
proxy: proxy:
external: true external: true
backend: backend:
internal: true internal: true

18
lib/redis.ts Normal file
View File

@ -0,0 +1,18 @@
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

@ -9,7 +9,7 @@ const redis = new Redis({
}); });
export const config = { export const config = {
runtime: "edge", // Возможно потребуется изменить на "nodejs" если возникнут проблемы runtime: "nodejs", // Возможно потребуется изменить на "nodejs" если возникнут проблемы
}; };
export default async function incr(req: NextRequest): Promise<NextResponse> { export default async function incr(req: NextRequest): Promise<NextResponse> {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB