mirror of
https://github.com/XFox111/my-website.git
synced 2026-04-22 07:28:01 +03:00
init: First version
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import resumeList, { Resume } from "@/_data/resumeList";
|
||||
import { NextRequest } from "next/server";
|
||||
import { PDFDocument, PDFPage } from "pdf-lib";
|
||||
|
||||
export async function GET(req: NextRequest): Promise<Response>
|
||||
{
|
||||
const type: string | null = req.nextUrl.searchParams.get("type");
|
||||
const resume: Resume | undefined = findResume(type);
|
||||
|
||||
if (!resume)
|
||||
return error(400, "'type' parameter is invalid");
|
||||
|
||||
if (!process.env.RESUME_URL)
|
||||
return error(500, "Cannot find file location.");
|
||||
|
||||
try
|
||||
{
|
||||
// Fetch the PDF file from the remote URL using the fetch API
|
||||
const response: Response = await fetch(process.env.RESUME_URL as string);
|
||||
|
||||
if (!response.ok)
|
||||
return error(500, "Failed to fetch PDF file");
|
||||
|
||||
// Load the PDF document
|
||||
const pdfData: ArrayBuffer = await response.arrayBuffer();
|
||||
const srcDoc: PDFDocument = await PDFDocument.load(pdfData);
|
||||
|
||||
// Create a new PDF document with the specified page
|
||||
const newDoc: PDFDocument = await PDFDocument.create();
|
||||
const [page]: PDFPage[] = await newDoc.copyPages(srcDoc, [resume.pageIndex]);
|
||||
newDoc.addPage(page);
|
||||
|
||||
// Serialize the new PDF document
|
||||
const pdfBytes: Uint8Array = await newDoc.save();
|
||||
|
||||
// Send the PDF page as a response
|
||||
return new Response(
|
||||
Buffer.from(pdfBytes),
|
||||
{
|
||||
// Set response headers for PDF file
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `inline; filename="${resume.fileName.replaceAll("\"", "'")}.pdf"`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Error processing PDF:", ex);
|
||||
return error(500, "Failed to process PDF file");
|
||||
}
|
||||
}
|
||||
|
||||
const findResume = (type: string | null): Resume | undefined =>
|
||||
resumeList.find(i => i.key === type) ?? resumeList.find(i => i.default);
|
||||
|
||||
const error = (status: number, message: string): Response =>
|
||||
new Response(message, { status });
|
||||
@@ -0,0 +1,89 @@
|
||||
@import "../theme.scss";
|
||||
|
||||
.page
|
||||
{
|
||||
@include flex(column);
|
||||
@include align(center, center);
|
||||
@include subtitle1($fontFamilyBaseAlt);
|
||||
text-align: center;
|
||||
gap: $spacingXXXL;
|
||||
|
||||
h1
|
||||
{
|
||||
@include title1($fontFamilyBaseAlt);
|
||||
}
|
||||
|
||||
.resumeButtons
|
||||
{
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr;
|
||||
gap: $spacingXL;
|
||||
|
||||
.button
|
||||
{
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
|
||||
.image
|
||||
{
|
||||
height: 128px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 860px)
|
||||
{
|
||||
grid-auto-flow: row;
|
||||
|
||||
.button
|
||||
{
|
||||
flex-flow: row;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
|
||||
.image
|
||||
{
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkedin
|
||||
{
|
||||
position: relative;
|
||||
background-image: none;
|
||||
overflow: clip;
|
||||
|
||||
> i
|
||||
{
|
||||
height: 32px !important;
|
||||
width: 32px !important;
|
||||
}
|
||||
|
||||
.circle
|
||||
{
|
||||
position: absolute;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: $borderRadiusCircular;
|
||||
left: 16px;
|
||||
z-index: -1;
|
||||
|
||||
background-color: var(--network-color);
|
||||
transition: transform $durationFast $curveEasyEaseMax;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover
|
||||
{
|
||||
color: $colorNeutralForegroundStaticInverted;
|
||||
|
||||
.circle
|
||||
{
|
||||
transform: scale(1100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Button from "@/_components/Button";
|
||||
import links from "@/_data/links";
|
||||
import { getTitle } from "@/_data/metadata";
|
||||
import resumeList from "@/_data/resumeList";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { SocialIcon, social_icons } from "react-social-icons";
|
||||
import cls from "./page.module.scss";
|
||||
|
||||
export const metadata: Metadata =
|
||||
{
|
||||
title: getTitle("Resume")
|
||||
};
|
||||
|
||||
// [SPECIAL]
|
||||
|
||||
const ResumePage: React.FC = () => (
|
||||
<main className={ cls.page }>
|
||||
|
||||
<h1>Who are you looking for?</h1>
|
||||
<div className={ cls.resumeButtons }>
|
||||
{ resumeList.map(i =>
|
||||
<Button key={ i.key } className={ cls.button }
|
||||
href={ `/resume/download?type=${i.key}` } download
|
||||
icon={
|
||||
<Image className={ cls.image } src={ i.image.src } priority draggable={ false }
|
||||
aria-hidden alt={ i.image.alt } />
|
||||
}>
|
||||
|
||||
{ i.label }
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
|
||||
{ links.linkedin &&
|
||||
<>
|
||||
<p>You can also check out my</p>
|
||||
<Button href={ links.linkedin } target="_blank"
|
||||
className={ cls.linkedin }
|
||||
// @ts-expect-error Inline variables are not supported by TS definition, although they work as intended.
|
||||
style={ { "--network-color": social_icons.get("linkedin")!.color } }
|
||||
icon={ <SocialIcon network="linkedin" as="i" aria-hidden /> }>
|
||||
|
||||
LinkedIn profile
|
||||
<span className={ cls.circle } />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
</main>
|
||||
);
|
||||
|
||||
export default ResumePage;
|
||||
Reference in New Issue
Block a user