Dev #1
14
.idea/workspace.xml
generated
14
.idea/workspace.xml
generated
@@ -5,8 +5,15 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="2a96f817-9dc2-4f3c-893a-c4974c750774" name="Changes" comment="">
|
<list default="true" id="2a96f817-9dc2-4f3c-893a-c4974c750774" name="Changes" comment="">
|
||||||
|
<change afterPath="$PROJECT_DIR$/public/images/ogBg.png" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/app/api/og-image/lib/render.ts" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/app/api/og-image/lib/template.tsx" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/app/api/og-image/lib/types.ts" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/app/api/og-image/route.ts" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/app/api/og/route.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/app/page.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/app/page.tsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -14,16 +21,16 @@
|
|||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="DarkyenusTimeTracker">
|
<component name="DarkyenusTimeTracker">
|
||||||
<option name="totalTimeSeconds" value="212238" />
|
<option name="totalTimeSeconds" value="219894" />
|
||||||
<option name="gitIntegration" value="true" />
|
<option name="gitIntegration" value="true" />
|
||||||
<option name="naggedAbout" value="1" />
|
<option name="naggedAbout" value="1" />
|
||||||
</component>
|
</component>
|
||||||
<component name="FileTemplateManagerImpl">
|
<component name="FileTemplateManagerImpl">
|
||||||
<option name="RECENT_TEMPLATES">
|
<option name="RECENT_TEMPLATES">
|
||||||
<list>
|
<list>
|
||||||
<option value="TypeScript File" />
|
|
||||||
<option value="SCSS File" />
|
<option value="SCSS File" />
|
||||||
<option value="TypeScript JSX File" />
|
<option value="TypeScript JSX File" />
|
||||||
|
<option value="TypeScript File" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
@@ -70,6 +77,7 @@
|
|||||||
}</component>
|
}</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="C:\dev-personal\ocenka-web\public\images" />
|
||||||
<recent name="C:\dev-personal\ocenka-web\src\widgets" />
|
<recent name="C:\dev-personal\ocenka-web\src\widgets" />
|
||||||
<recent name="C:\dev-personal\ocenka-web\src\widgets\sidebar" />
|
<recent name="C:\dev-personal\ocenka-web\src\widgets\sidebar" />
|
||||||
<recent name="C:\dev-personal\ocenka-web\src\views\home" />
|
<recent name="C:\dev-personal\ocenka-web\src\views\home" />
|
||||||
|
|||||||
1000
package-lock.json
generated
1000
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
|||||||
"libphonenumber-js": "^1.12.9",
|
"libphonenumber-js": "^1.12.9",
|
||||||
"next": "15.5.2",
|
"next": "15.5.2",
|
||||||
"nodemailer": "7.0.11",
|
"nodemailer": "7.0.11",
|
||||||
|
"puppeteer": "^24.32.0",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
|
|||||||
BIN
public/images/ogBg.png
Normal file
BIN
public/images/ogBg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
public/images/only-eagle-logo.png
Normal file
BIN
public/images/only-eagle-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
10
src/app/api/og-image/lib/render.ts
Normal file
10
src/app/api/og-image/lib/render.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import core from 'puppeteer';
|
||||||
|
|
||||||
|
export async function getScreenshot(html: string) {
|
||||||
|
const browser = await core.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 0.5 });
|
||||||
|
await page.setContent(html);
|
||||||
|
return await page.screenshot({ type: 'png' });
|
||||||
|
}
|
||||||
38
src/app/api/og-image/lib/template.tsx
Normal file
38
src/app/api/og-image/lib/template.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { ParsedRequest } from './types';
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
export const OGImage: FC<ParsedRequest> = ({ title, desc }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
background: 'linear-gradient(150deg, #58C644, #3f87a6)',
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: '160px', left: '600px' }}>
|
||||||
|
<h1
|
||||||
|
style={{ color: 'white', fontFamily: 'sans-serif', fontSize: '80px' }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
style={{ color: 'white', fontFamily: 'sans-serif', fontSize: '40px' }}
|
||||||
|
>
|
||||||
|
{desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getHtml(parsedReq: ParsedRequest) {
|
||||||
|
const { title, desc } = parsedReq;
|
||||||
|
const { renderToString } = await import('react-dom/server');
|
||||||
|
|
||||||
|
const result = renderToString(<OGImage title={title} desc={desc} />);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
4
src/app/api/og-image/lib/types.ts
Normal file
4
src/app/api/og-image/lib/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ParsedRequest {
|
||||||
|
title?: string;
|
||||||
|
desc?: string;
|
||||||
|
}
|
||||||
33
src/app/api/og-image/route.ts
Normal file
33
src/app/api/og-image/route.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { getHtml } from './lib/template';
|
||||||
|
import { getScreenshot } from './lib/render';
|
||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const params = Object.fromEntries(request.nextUrl.searchParams);
|
||||||
|
const html = await getHtml(params);
|
||||||
|
const file = await getScreenshot(html);
|
||||||
|
|
||||||
|
return new Response(file, {
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
headers: {
|
||||||
|
// 'Access-Control-Allow-Origin': '*',
|
||||||
|
// 'Access-Control-Allow-Methods': 'GET',
|
||||||
|
'Content-Type': `image/png`,
|
||||||
|
'Cache-Control': `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return new Response(
|
||||||
|
'<h1>Internal Error</h1><p>Sorry, there was a problem</p>',
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/app/api/og/route.ts
Normal file
53
src/app/api/og/route.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const title = req.nextUrl.searchParams.get('title') ?? 'Default title';
|
||||||
|
const desc = req.nextUrl.searchParams.get('desc') ?? 'Default description';
|
||||||
|
|
||||||
|
const imageUrl = `${req.nextUrl.origin}/images/ogBg.png`;
|
||||||
|
const logoUrl = `${req.nextUrl.origin}/images/logo-dtr-white.png`;
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<html lang="ru-RU">
|
||||||
|
<body style="
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 600px;
|
||||||
|
height: 315px;
|
||||||
|
background: #111;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<img src="${imageUrl}" style="object-fit: cover; width:100%; height:100%; opacity:0.6; position:absolute; left:0; top:0;" />
|
||||||
|
<img src="${logoUrl}" style="width: 288px; height: 89px; position:absolute; left:20px; top:20px;" />
|
||||||
|
<div style="position: absolute; top: 110px; left: 280px; width:100%; height:100%;">
|
||||||
|
<h2 style="color: white; font-family: sans-serif; font-size: 42px">${title}</h2>
|
||||||
|
<p style="color: white; font-family: sans-serif; font-size: 24px">${desc}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Запуск браузера
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setContent(html, { waitUntil: 'networkidle0' });
|
||||||
|
await page.setViewport({ width: 600, height: 315 });
|
||||||
|
const screenshot = await page.screenshot({ type: 'png' });
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return new NextResponse(screenshot, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
'Cache-Control': 'max-age=60',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user