mirror of
https://github.com/XFox111/my-website.git
synced 2026-04-22 07:28:01 +03:00
chore(deps): dependency bumps + related refactoring (fixes #19)
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off",
|
||||
"indent": [
|
||||
"warn",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-unused-vars": "warn",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"no-unreachable": "warn",
|
||||
"no-empty": "warn"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.alertBox
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.button
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.banner
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.footer
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.header
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.navigation
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.dialog
|
||||
{
|
||||
@@ -15,6 +15,9 @@
|
||||
color: unset;
|
||||
border: none;
|
||||
|
||||
transition: right $durationNormal $curveEasyEaseMax;
|
||||
right: -350px;
|
||||
|
||||
&::backdrop
|
||||
{
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
@@ -64,9 +67,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
transition: right $durationNormal $curveEasyEaseMax;
|
||||
right: -350px;
|
||||
|
||||
&.show
|
||||
{
|
||||
right: 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.socials
|
||||
{
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.section
|
||||
{
|
||||
@include centerTwo;
|
||||
min-height: 75vh;
|
||||
align-items: end;
|
||||
|
||||
@include centerTwo;
|
||||
|
||||
.content
|
||||
{
|
||||
@include flex(column);
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
const ThirdPartyAttribution: React.FC = () => <>
|
||||
<p>
|
||||
Iconography, typography, and color palette of this website are based on
|
||||
Microsoft's <a target="_blank" href="https://fluentui.dev/">Fluent Design System</a> licensed under
|
||||
Microsoft's <a target="_blank" href="https://fluentui.dev/">Fluent Design System</a> licensed under
|
||||
the <a target="_blank" href="https://github.com/microsoft/fluentui/blob/main/LICENSE">MIT License</a>.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.title
|
||||
{
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.section
|
||||
{
|
||||
@include centerTwo;
|
||||
@include body2($fontFamilyBaseAlt);
|
||||
color: $colorNeutralForeground2;
|
||||
align-items: center;
|
||||
|
||||
@include centerTwo;
|
||||
|
||||
> div:first-child
|
||||
{
|
||||
@include flex(column);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.section
|
||||
{
|
||||
|
||||
@@ -3,28 +3,31 @@
|
||||
import Button from "@/_components/Button";
|
||||
import SocialLinks from "@/_components/SocialLinks";
|
||||
import contacts from "@/_data/contacts";
|
||||
import FormStatusTracker from "@/_utils/FormStatusTracker";
|
||||
import React, { InputHTMLAttributes, useEffect, useMemo, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import { getSitekey } from "@/_utils/turnstile";
|
||||
import React, { InputHTMLAttributes, useActionState, useEffect, useMemo, useState } from "react";
|
||||
import Turnstile from "react-turnstile";
|
||||
import sendInquiry, { FormStatus } from "../_utils/sendInquiry";
|
||||
import cls from "./ContactSection.module.scss";
|
||||
import { getSitekey } from "@/_utils/turnstile";
|
||||
|
||||
const defaultState: FormStatus = { status: "idle" };
|
||||
|
||||
const ContactSection: React.FC = () =>
|
||||
{
|
||||
const [pending, setPending] = useState<boolean>(false);
|
||||
const [{ status, message }, formAction] = useFormState<FormStatus, FormData>(sendInquiry, defaultState);
|
||||
// Added state-backing for the form fields to prevent it from resetting on submit
|
||||
// See https://github.com/facebook/react/issues/29034
|
||||
// FIXME: Remove form state once #29034 is fixed
|
||||
const [form, setForm] = useState<{ email: string, subject: string, message: string; }>({ email: "", subject: "", message: "" });
|
||||
|
||||
const [{ status, message }, formAction, isPending] = useActionState<FormStatus, FormData>(sendInquiry, defaultState);
|
||||
const { telephone: phone, email, socials } = contacts;
|
||||
const [cfSitekey, setCfSitekey] = useState<string | undefined | null>(null);
|
||||
|
||||
const sharedProps: InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> = useMemo(() => ({
|
||||
required: true,
|
||||
disabled: pending,
|
||||
readOnly: status === "success"
|
||||
}), [status, pending]);
|
||||
disabled: isPending,
|
||||
readOnly: status === "success",
|
||||
"data-clarity-mask": true
|
||||
}), [status, isPending]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -33,7 +36,7 @@ const ContactSection: React.FC = () =>
|
||||
|
||||
return (
|
||||
<section id="contacts" className={ cls.section }>
|
||||
<h2>Let's get in touch</h2>
|
||||
<h2>Let's get in touch</h2>
|
||||
|
||||
<div className={ cls.content }>
|
||||
|
||||
@@ -41,19 +44,20 @@ const ContactSection: React.FC = () =>
|
||||
<h3>Inquiries, requests or proposals</h3>
|
||||
|
||||
<form className={ cls.container } action={ formAction }>
|
||||
<FormStatusTracker onPendingChanged={ setPending } />
|
||||
|
||||
<input name="email" type="email" { ...sharedProps } data-clarity-mask
|
||||
<input name="email" type="email" { ...sharedProps }
|
||||
value={ form.email } onChange={ e => setForm({ ...form, email: e.target.value }) }
|
||||
autoComplete="email" spellCheck="false"
|
||||
maxLength={ 60 }
|
||||
placeholder="Email" />
|
||||
|
||||
<input name="subject" type="text" { ...sharedProps } data-clarity-mask
|
||||
<input name="subject" type="text" { ...sharedProps }
|
||||
value={ form.subject } onChange={ e => setForm({ ...form, subject: e.target.value }) }
|
||||
autoComplete="off" spellCheck="true"
|
||||
maxLength={ 120 }
|
||||
placeholder="Subject" />
|
||||
|
||||
<textarea name="message" { ...sharedProps } className={ cls.textarea } data-clarity-mask
|
||||
<textarea name="message" { ...sharedProps } className={ cls.textarea }
|
||||
value={ form.message } onChange={ e => setForm({ ...form, message: e.target.value }) }
|
||||
autoComplete="off" spellCheck="true"
|
||||
minLength={ 100 } maxLength={ 2000 }
|
||||
placeholder="Message (min 100 characters)" />
|
||||
@@ -72,18 +76,18 @@ const ContactSection: React.FC = () =>
|
||||
</div>
|
||||
|
||||
<div className={ cls.formToolbar }>
|
||||
<div className={ `${cls.status} ${pending ? "" : cls[status]}` }>
|
||||
{ pending &&
|
||||
<div className={ `${cls.status} ${isPending ? "" : cls[status]}` }>
|
||||
{ isPending &&
|
||||
<span role="alert" aria-live="assertive">
|
||||
Sending
|
||||
</span>
|
||||
}
|
||||
{ !pending && status === "success" &&
|
||||
{ !isPending && status === "success" &&
|
||||
<span role="alert" aria-live="assertive">
|
||||
Message successfully sent
|
||||
</span>
|
||||
}
|
||||
{ !pending && status === "error" &&
|
||||
{ !isPending && status === "error" &&
|
||||
<span role="alert" aria-live="assertive">
|
||||
{ message ?? "Something went wrong" }
|
||||
</span>
|
||||
@@ -91,7 +95,7 @@ const ContactSection: React.FC = () =>
|
||||
</div>
|
||||
|
||||
{ status !== "success" &&
|
||||
<Button type="submit" disabled={ pending }>
|
||||
<Button type="submit" disabled={ isPending }>
|
||||
Submit
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.section
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ const ExperienceSection: React.FC = () => (
|
||||
|
||||
function getAriaLabel(item: WorkplaceEntry): string
|
||||
{
|
||||
let str: string[] = [];
|
||||
const str: string[] = [];
|
||||
|
||||
if (item.year)
|
||||
str.push(`${item.year} -`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.section
|
||||
{
|
||||
@@ -30,8 +30,9 @@
|
||||
|
||||
.defaultImg
|
||||
{
|
||||
@include slideIn;
|
||||
align-self: center;
|
||||
|
||||
@include slideIn;
|
||||
}
|
||||
|
||||
.projectItem
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.section
|
||||
{
|
||||
@include centerTwo;
|
||||
align-items: center;
|
||||
|
||||
@include centerTwo;
|
||||
|
||||
.illustrations
|
||||
{
|
||||
justify-self: center;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.list
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "./theme.scss";
|
||||
@use "./theme" as *;
|
||||
|
||||
:root
|
||||
{
|
||||
@@ -35,6 +35,12 @@ body
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: geometricPrecision;
|
||||
|
||||
background-color: $colorNeutralBackground1;
|
||||
color: $colorNeutralForeground1;
|
||||
|
||||
> main
|
||||
{
|
||||
@include maxCenter;
|
||||
@@ -43,12 +49,6 @@ body
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: geometricPrecision;
|
||||
|
||||
background-color: $colorNeutralBackground1;
|
||||
color: $colorNeutralForeground1;
|
||||
|
||||
::selection
|
||||
{
|
||||
background-color: $colorNeutralBackgroundInverted;
|
||||
@@ -0,0 +1,5 @@
|
||||
@forward "dark";
|
||||
@forward "light";
|
||||
@forward "typography";
|
||||
@forward "mixins";
|
||||
@forward "tokens";
|
||||
@@ -1,3 +1,6 @@
|
||||
@use "tokens" as *;
|
||||
@use "typography" as *;
|
||||
|
||||
@mixin maxCenter($maxWidth: 1400px)
|
||||
{
|
||||
align-self: center;
|
||||
@@ -1,8 +1,3 @@
|
||||
@import "./theme.dark.scss";
|
||||
@import "./theme.light.scss";
|
||||
@import "./theme.typography.scss";
|
||||
@import "./theme.mixins.scss";
|
||||
|
||||
// Border radius
|
||||
$borderRadiusNone: 0;
|
||||
$borderRadiusSmall: 2px;
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "tokens" as *;
|
||||
|
||||
@mixin caption2($font: $fontFamilyBase)
|
||||
{
|
||||
@include setTypography($font, $fontSizeBase100, $fontWeightRegular, $lineHeightBase100);
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
// Since useFormStatus requires to be inside of the form, I moved it to a separate helper component.
|
||||
// Could it be done better? Probably.
|
||||
// Did I do it? No.
|
||||
|
||||
/** Renders a React component that tracks the form status and calls the provided callback function when the form status changes. */
|
||||
const FormStatusTracker: React.FC<FormStatusTrackerProps> = ({ onPendingChanged }) =>
|
||||
{
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
onPendingChanged(pending);
|
||||
}, [pending, onPendingChanged]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default FormStatusTracker;
|
||||
|
||||
/** Props for the `FormStatusTracker` component. */
|
||||
export type FormStatusTrackerProps =
|
||||
{
|
||||
/** The callback function that is called when the form status changes. */
|
||||
// For some reason ESLint shows a warning for "unused" "pending" parameter.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onPendingChanged: (pending: boolean) => void;
|
||||
};
|
||||
Vendored
-1
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
declare global
|
||||
{
|
||||
interface Window
|
||||
|
||||
@@ -35,9 +35,9 @@ function setCookie(name: string, value: string | number, maxAge: number): void
|
||||
|
||||
function getCookie(name: string): string | undefined
|
||||
{
|
||||
let cookieName = name + "=";
|
||||
let rawCookies = decodeURIComponent(window.document.cookie);
|
||||
let cookies = rawCookies.split(";");
|
||||
const cookieName = name + "=";
|
||||
const rawCookies = decodeURIComponent(window.document.cookie);
|
||||
const cookies = rawCookies.split(";");
|
||||
|
||||
for (const cookie of cookies)
|
||||
if (cookie.trim().startsWith(cookieName))
|
||||
|
||||
@@ -43,8 +43,6 @@ export default async function sendInquiry(_: FormStatus, formData: FormData): Pr
|
||||
message: "Challenge has expired. Try again"
|
||||
};
|
||||
|
||||
console.error(error);
|
||||
|
||||
return {
|
||||
status: "error",
|
||||
message: "Something went wrong"
|
||||
|
||||
@@ -13,12 +13,11 @@ export async function verifyTurnstile(token: string): Promise<[false, TurnstileE
|
||||
return [true];
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
console.log(headers().get("CF-Connecting-IP"));
|
||||
const headerList = await headers();
|
||||
|
||||
formData.append("secret", process.env.CF_SECRET);
|
||||
formData.append("response", token);
|
||||
formData.append("remoteip", headers().get("CF-Connecting-IP") ?? "");
|
||||
formData.append("remoteip", headerList.get("CF-Connecting-IP") ?? "");
|
||||
|
||||
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.page
|
||||
{
|
||||
|
||||
@@ -42,13 +42,13 @@ const AttributionPage: React.FC = () => (
|
||||
see the <a href="https://privacy.microsoft.com/privacystatement" target="_blank">Microsoft Privacy Statement</a>.
|
||||
</p>
|
||||
<p>
|
||||
If the "Do Not Track" option is enabled in your browser,
|
||||
If the "Do Not Track" option is enabled in your browser,
|
||||
the website will not execute any tracking code.
|
||||
</p>
|
||||
{ requireExplicitConsent() &&
|
||||
<p>
|
||||
If you previously gave your consent to use cookies,
|
||||
you can revoke it by clicking "Revoke my consent" button on this page below
|
||||
you can revoke it by clicking "Revoke my consent" button on this page below
|
||||
(the button is available only if the consent was given).
|
||||
Recorded data will be deleted after 30-day retention period.
|
||||
</p>
|
||||
@@ -99,7 +99,7 @@ const AttributionPage: React.FC = () => (
|
||||
<p>Copyright © { new Date().getFullYear() } { Package.author.name }</p>
|
||||
<p>
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
@@ -110,7 +110,7 @@ const AttributionPage: React.FC = () => (
|
||||
copies or substantial portions of the Software.
|
||||
</p>
|
||||
<p>
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import Footer from "./_components/Footer";
|
||||
import Header from "./_components/Header";
|
||||
import { analyticsEnabled, requireExplicitConsent } from "./_utils/analytics/server";
|
||||
import fonts from "./fonts";
|
||||
import "./globals.scss";
|
||||
import "./_styles/globals.scss";
|
||||
|
||||
export const viewport: Viewport =
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "./theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.root
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "./theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.page
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "./theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.page
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../theme.scss";
|
||||
@use "@/_styles/theme" as *;
|
||||
|
||||
.page
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import js from "@eslint/js";
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: import.meta.dirname,
|
||||
recommendedConfig: js.configs.recommended
|
||||
});
|
||||
|
||||
const eslintConfig =
|
||||
[
|
||||
...compat.config({
|
||||
extends: ["eslint:recommended", "next/core-web-vitals", "next/typescript"],
|
||||
rules:
|
||||
{
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
"indent": [
|
||||
"warn",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,47 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("next-safe").nextSafe} */
|
||||
// @ts-ignore
|
||||
const nextSafe = require("next-safe");
|
||||
|
||||
/** @type {boolean} */
|
||||
const isDev = process.env.NODE_ENV !== "production";
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
productionBrowserSourceMaps: true,
|
||||
|
||||
async headers()
|
||||
{
|
||||
return [
|
||||
{
|
||||
source: "/((?!api|_next/static|_next/image|favicon.ico|clarity.js|icon.svg).*)",
|
||||
headers: nextSafe({
|
||||
isDev: false,
|
||||
contentSecurityPolicy:
|
||||
{
|
||||
"script-src": isDev ?
|
||||
"'self' 'unsafe-inline' https://*.clarity.ms https://c.bing.com https://*.cloudflare.com 'unsafe-eval'" :
|
||||
"'self' 'unsafe-inline' https://*.clarity.ms https://c.bing.com https://*.cloudflare.com",
|
||||
|
||||
"connect-src": isDev ?
|
||||
"'self' https://*.clarity.ms https://c.bing.com webpack://*" :
|
||||
"'self' https://*.clarity.ms https://c.bing.com",
|
||||
|
||||
"style-src": "'self' 'unsafe-inline'",
|
||||
|
||||
"frame-src": "https://*.cloudflare.com 'none'",
|
||||
|
||||
// @ts-ignore
|
||||
"prefetch-src": false
|
||||
},
|
||||
permissionsPolicy: false
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
@@ -0,0 +1,68 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {boolean} */
|
||||
const isDev = process.env.NODE_ENV !== "production";
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
productionBrowserSourceMaps: true,
|
||||
|
||||
async headers()
|
||||
{
|
||||
const cspPolicy = generateCspPolicy(isDev);
|
||||
|
||||
return [
|
||||
{
|
||||
source: "/((?!api|_next/static|_next/image|favicon.ico|clarity.js|icon.svg).*)",
|
||||
headers:
|
||||
[
|
||||
{ key: "Content-Security-Policy", value: cspPolicy },
|
||||
{ key: "X-Content-Security-Policy", value: cspPolicy },
|
||||
{ key: "X-WebKit-CSP", value: cspPolicy },
|
||||
{ key: "Referrer-Policy", value: "no-referrer" },
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "X-Frame-Options", value: "DENY" },
|
||||
{ key: "X-XSS-Protection", value: "1; mode=block" },
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {boolean} isDev
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateCspPolicy(isDev)
|
||||
{
|
||||
const csp =
|
||||
{
|
||||
"base-uri": "'none'",
|
||||
"child-src": "'none'",
|
||||
"connect-src": "'self' https://*.clarity.ms https://c.bing.com",
|
||||
"default-src": "'self'",
|
||||
"font-src": "'self'",
|
||||
"form-action": "'self'",
|
||||
"frame-ancestors": "'none'",
|
||||
"frame-src": "https://*.cloudflare.com",
|
||||
"img-src": "'self'",
|
||||
"manifest-src": "'self'",
|
||||
"media-src": "'self'",
|
||||
"object-src": "'none'",
|
||||
"script-src": "'self' https://*.clarity.ms https://c.bing.com https://*.cloudflare.com 'unsafe-inline'",
|
||||
"style-src": "'self' 'unsafe-inline'",
|
||||
"worker-src": "'self'"
|
||||
};
|
||||
|
||||
if (isDev)
|
||||
{
|
||||
csp["connect-src"] = [csp["connect-src"].trim(), "webpack://*"].join(" ");
|
||||
csp["script-src"] = [csp["script-src"].trim(), "'unsafe-eval'"].join(" ");
|
||||
}
|
||||
|
||||
return Object.entries(csp).map(i => i.join(" ")).join(";");
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
+18
-14
@@ -21,26 +21,30 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-icons": "^2.0.270",
|
||||
"next": "14.2.21",
|
||||
"next-safe": "^3.5.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"@fluentui/react-icons": "^2.0.271",
|
||||
"next": "^15.1.6",
|
||||
"nodemailer": "^6.10.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-social-icons": "^6.18.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-social-icons": "^6.20.0",
|
||||
"react-turnstile": "^1.1.4",
|
||||
"sass": "^1.83.1",
|
||||
"sharp": "^0.33.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"@next/eslint-plugin-next": "^15.1.6",
|
||||
"@types/node": "^22.10.10",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^15.1.4",
|
||||
"typescript": "^5"
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-next": "^15.1.6",
|
||||
"sass": "^1.83.4",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user