1
0
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:
2025-01-26 21:50:56 +00:00
parent 9d369ad4d2
commit 457fbf33e5
44 changed files with 657 additions and 601 deletions
-36
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.alertBox .alertBox
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.button .button
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.banner .banner
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.footer .footer
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.header .header
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.navigation .navigation
{ {
+4 -4
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.dialog .dialog
{ {
@@ -15,6 +15,9 @@
color: unset; color: unset;
border: none; border: none;
transition: right $durationNormal $curveEasyEaseMax;
right: -350px;
&::backdrop &::backdrop
{ {
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
@@ -64,9 +67,6 @@
} }
} }
transition: right $durationNormal $curveEasyEaseMax;
right: -350px;
&.show &.show
{ {
right: 0; right: 0;
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.socials .socials
{ {
+3 -2
View File
@@ -1,11 +1,12 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.section .section
{ {
@include centerTwo;
min-height: 75vh; min-height: 75vh;
align-items: end; align-items: end;
@include centerTwo;
.content .content
{ {
@include flex(column); @include flex(column);
+1 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
const ThirdPartyAttribution: React.FC = () => <> const ThirdPartyAttribution: React.FC = () => <>
<p> <p>
Iconography, typography, and color palette of this website are based on 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&apos;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>. the <a target="_blank" href="https://github.com/microsoft/fluentui/blob/main/LICENSE">MIT License</a>.
</p> </p>
<p> <p>
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.title .title
{ {
+3 -2
View File
@@ -1,12 +1,13 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.section .section
{ {
@include centerTwo;
@include body2($fontFamilyBaseAlt); @include body2($fontFamilyBaseAlt);
color: $colorNeutralForeground2; color: $colorNeutralForeground2;
align-items: center; align-items: center;
@include centerTwo;
> div:first-child > div:first-child
{ {
@include flex(column); @include flex(column);
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.section .section
{ {
+24 -20
View File
@@ -3,28 +3,31 @@
import Button from "@/_components/Button"; import Button from "@/_components/Button";
import SocialLinks from "@/_components/SocialLinks"; import SocialLinks from "@/_components/SocialLinks";
import contacts from "@/_data/contacts"; import contacts from "@/_data/contacts";
import FormStatusTracker from "@/_utils/FormStatusTracker"; import { getSitekey } from "@/_utils/turnstile";
import React, { InputHTMLAttributes, useEffect, useMemo, useState } from "react"; import React, { InputHTMLAttributes, useActionState, useEffect, useMemo, useState } from "react";
import { useFormState } from "react-dom";
import Turnstile from "react-turnstile"; import Turnstile from "react-turnstile";
import sendInquiry, { FormStatus } from "../_utils/sendInquiry"; import sendInquiry, { FormStatus } from "../_utils/sendInquiry";
import cls from "./ContactSection.module.scss"; import cls from "./ContactSection.module.scss";
import { getSitekey } from "@/_utils/turnstile";
const defaultState: FormStatus = { status: "idle" }; const defaultState: FormStatus = { status: "idle" };
const ContactSection: React.FC = () => const ContactSection: React.FC = () =>
{ {
const [pending, setPending] = useState<boolean>(false); // Added state-backing for the form fields to prevent it from resetting on submit
const [{ status, message }, formAction] = useFormState<FormStatus, FormData>(sendInquiry, defaultState); // 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 { telephone: phone, email, socials } = contacts;
const [cfSitekey, setCfSitekey] = useState<string | undefined | null>(null); const [cfSitekey, setCfSitekey] = useState<string | undefined | null>(null);
const sharedProps: InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> = useMemo(() => ({ const sharedProps: InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> = useMemo(() => ({
required: true, required: true,
disabled: pending, disabled: isPending,
readOnly: status === "success" readOnly: status === "success",
}), [status, pending]); "data-clarity-mask": true
}), [status, isPending]);
useEffect(() => useEffect(() =>
{ {
@@ -33,7 +36,7 @@ const ContactSection: React.FC = () =>
return ( return (
<section id="contacts" className={ cls.section }> <section id="contacts" className={ cls.section }>
<h2>Let's get in touch</h2> <h2>Let&apos;s get in touch</h2>
<div className={ cls.content }> <div className={ cls.content }>
@@ -41,19 +44,20 @@ const ContactSection: React.FC = () =>
<h3>Inquiries, requests or proposals</h3> <h3>Inquiries, requests or proposals</h3>
<form className={ cls.container } action={ formAction }> <form className={ cls.container } action={ formAction }>
<FormStatusTracker onPendingChanged={ setPending } /> <input name="email" type="email" { ...sharedProps }
value={ form.email } onChange={ e => setForm({ ...form, email: e.target.value }) }
<input name="email" type="email" { ...sharedProps } data-clarity-mask
autoComplete="email" spellCheck="false" autoComplete="email" spellCheck="false"
maxLength={ 60 } maxLength={ 60 }
placeholder="Email" /> 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" autoComplete="off" spellCheck="true"
maxLength={ 120 } maxLength={ 120 }
placeholder="Subject" /> 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" autoComplete="off" spellCheck="true"
minLength={ 100 } maxLength={ 2000 } minLength={ 100 } maxLength={ 2000 }
placeholder="Message (min 100 characters)" /> placeholder="Message (min 100 characters)" />
@@ -72,18 +76,18 @@ const ContactSection: React.FC = () =>
</div> </div>
<div className={ cls.formToolbar }> <div className={ cls.formToolbar }>
<div className={ `${cls.status} ${pending ? "" : cls[status]}` }> <div className={ `${cls.status} ${isPending ? "" : cls[status]}` }>
{ pending && { isPending &&
<span role="alert" aria-live="assertive"> <span role="alert" aria-live="assertive">
Sending Sending
</span> </span>
} }
{ !pending && status === "success" && { !isPending && status === "success" &&
<span role="alert" aria-live="assertive"> <span role="alert" aria-live="assertive">
Message successfully sent Message successfully sent
</span> </span>
} }
{ !pending && status === "error" && { !isPending && status === "error" &&
<span role="alert" aria-live="assertive"> <span role="alert" aria-live="assertive">
{ message ?? "Something went wrong" } { message ?? "Something went wrong" }
</span> </span>
@@ -91,7 +95,7 @@ const ContactSection: React.FC = () =>
</div> </div>
{ status !== "success" && { status !== "success" &&
<Button type="submit" disabled={ pending }> <Button type="submit" disabled={ isPending }>
Submit Submit
</Button> </Button>
} }
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.section .section
{ {
+1 -1
View File
@@ -37,7 +37,7 @@ const ExperienceSection: React.FC = () => (
function getAriaLabel(item: WorkplaceEntry): string function getAriaLabel(item: WorkplaceEntry): string
{ {
let str: string[] = []; const str: string[] = [];
if (item.year) if (item.year)
str.push(`${item.year} -`); str.push(`${item.year} -`);
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.section .section
{ {
@@ -30,8 +30,9 @@
.defaultImg .defaultImg
{ {
@include slideIn;
align-self: center; align-self: center;
@include slideIn;
} }
.projectItem .projectItem
+3 -2
View File
@@ -1,10 +1,11 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.section .section
{ {
@include centerTwo;
align-items: center; align-items: center;
@include centerTwo;
.illustrations .illustrations
{ {
justify-self: center; justify-self: center;
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.list .list
{ {
@@ -1,4 +1,4 @@
@import "./theme.scss"; @use "./theme" as *;
:root :root
{ {
@@ -35,6 +35,12 @@ body
max-width: 100vw; max-width: 100vw;
overflow-x: hidden; overflow-x: hidden;
font-synthesis: none;
text-rendering: geometricPrecision;
background-color: $colorNeutralBackground1;
color: $colorNeutralForeground1;
> main > main
{ {
@include maxCenter; @include maxCenter;
@@ -43,12 +49,6 @@ body
margin-top: 56px; margin-top: 56px;
} }
font-synthesis: none;
text-rendering: geometricPrecision;
background-color: $colorNeutralBackground1;
color: $colorNeutralForeground1;
::selection ::selection
{ {
background-color: $colorNeutralBackgroundInverted; background-color: $colorNeutralBackgroundInverted;
+5
View File
@@ -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) @mixin maxCenter($maxWidth: 1400px)
{ {
align-self: center; 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 // Border radius
$borderRadiusNone: 0; $borderRadiusNone: 0;
$borderRadiusSmall: 2px; $borderRadiusSmall: 2px;
@@ -1,3 +1,5 @@
@use "tokens" as *;
@mixin caption2($font: $fontFamilyBase) @mixin caption2($font: $fontFamilyBase)
{ {
@include setTypography($font, $fontSizeBase100, $fontWeightRegular, $lineHeightBase100); @include setTypography($font, $fontSizeBase100, $fontWeightRegular, $lineHeightBase100);
-32
View File
@@ -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;
};
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars */
declare global declare global
{ {
interface Window interface Window
+3 -3
View File
@@ -35,9 +35,9 @@ function setCookie(name: string, value: string | number, maxAge: number): void
function getCookie(name: string): string | undefined function getCookie(name: string): string | undefined
{ {
let cookieName = name + "="; const cookieName = name + "=";
let rawCookies = decodeURIComponent(window.document.cookie); const rawCookies = decodeURIComponent(window.document.cookie);
let cookies = rawCookies.split(";"); const cookies = rawCookies.split(";");
for (const cookie of cookies) for (const cookie of cookies)
if (cookie.trim().startsWith(cookieName)) if (cookie.trim().startsWith(cookieName))
-2
View File
@@ -43,8 +43,6 @@ export default async function sendInquiry(_: FormStatus, formData: FormData): Pr
message: "Challenge has expired. Try again" message: "Challenge has expired. Try again"
}; };
console.error(error);
return { return {
status: "error", status: "error",
message: "Something went wrong" message: "Something went wrong"
+2 -3
View File
@@ -13,12 +13,11 @@ export async function verifyTurnstile(token: string): Promise<[false, TurnstileE
return [true]; return [true];
const formData = new FormData(); const formData = new FormData();
const headerList = await headers();
console.log(headers().get("CF-Connecting-IP"));
formData.append("secret", process.env.CF_SECRET); formData.append("secret", process.env.CF_SECRET);
formData.append("response", token); 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", const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.page .page
{ {
+4 -4
View File
@@ -42,13 +42,13 @@ const AttributionPage: React.FC = () => (
see the <a href="https://privacy.microsoft.com/privacystatement" target="_blank">Microsoft Privacy Statement</a>. see the <a href="https://privacy.microsoft.com/privacystatement" target="_blank">Microsoft Privacy Statement</a>.
</p> </p>
<p> <p>
If the "Do Not Track" option is enabled in your browser, If the &quot;Do Not Track&quot; option is enabled in your browser,
the website will not execute any tracking code. the website will not execute any tracking code.
</p> </p>
{ requireExplicitConsent() && { requireExplicitConsent() &&
<p> <p>
If you previously gave your consent to use cookies, 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 &quot;Revoke my consent&quot; button on this page below
(the button is available only if the consent was given). (the button is available only if the consent was given).
Recorded data will be deleted after 30-day retention period. Recorded data will be deleted after 30-day retention period.
</p> </p>
@@ -99,7 +99,7 @@ const AttributionPage: React.FC = () => (
<p>Copyright &copy; { new Date().getFullYear() } { Package.author.name }</p> <p>Copyright &copy; { new Date().getFullYear() } { Package.author.name }</p>
<p> <p>
Permission is hereby granted, free of charge, to any person obtaining a copy 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 &quot;Software&quot;), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is 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. copies or substantial portions of the Software.
</p> </p>
<p> <p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+1 -1
View File
@@ -7,7 +7,7 @@ import Footer from "./_components/Footer";
import Header from "./_components/Header"; import Header from "./_components/Header";
import { analyticsEnabled, requireExplicitConsent } from "./_utils/analytics/server"; import { analyticsEnabled, requireExplicitConsent } from "./_utils/analytics/server";
import fonts from "./fonts"; import fonts from "./fonts";
import "./globals.scss"; import "./_styles/globals.scss";
export const viewport: Viewport = export const viewport: Viewport =
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "./theme.scss"; @use "@/_styles/theme" as *;
.root .root
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "./theme.scss"; @use "@/_styles/theme" as *;
.page .page
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "./theme.scss"; @use "@/_styles/theme" as *;
.page .page
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../theme.scss"; @use "@/_styles/theme" as *;
.page .page
{ {
+36
View File
@@ -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;
-47
View File
@@ -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;
+68
View File
@@ -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
View File
@@ -21,26 +21,30 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@fluentui/react-icons": "^2.0.270", "@fluentui/react-icons": "^2.0.271",
"next": "14.2.21", "next": "^15.1.6",
"next-safe": "^3.5.0", "nodemailer": "^6.10.0",
"nodemailer": "^6.9.16",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"react": "^18", "react": "^19.0.0",
"react-dom": "^18", "react-dom": "^19.0.0",
"react-social-icons": "^6.18.0", "react-social-icons": "^6.20.0",
"react-turnstile": "^1.1.4", "react-turnstile": "^1.1.4",
"sass": "^1.83.1",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22", "@next/eslint-plugin-next": "^15.1.6",
"@types/node": "^22.10.10",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/react": "^18", "@types/react": "^19.0.8",
"@types/react-dom": "^18", "@types/react-dom": "^19.0.3",
"eslint": "^8", "eslint": "^9.19.0",
"eslint-config-next": "^15.1.4", "eslint-config-next": "^15.1.6",
"typescript": "^5" "sass": "^1.83.4",
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0"
},
"resolutions": {
"react": "^19.0.0"
} }
} }
+450 -396
View File
File diff suppressed because it is too large Load Diff