Skip to content

Commit

Permalink
Merge pull request #282 from 0x74h51N/Staging
Browse files Browse the repository at this point in the history
server action cookieConsent & lang prefix routing
  • Loading branch information
0x74h51N committed Aug 27, 2024
2 parents eaa0476 + 7801817 commit e1c4286
Show file tree
Hide file tree
Showing 53 changed files with 352 additions and 252 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Introduction

This project began with the goal of creating a personal website to showcase my skills and portfolio. I have chosen "CrunchyPix" as the website and brand name. This website supports multiple languages through i18n and was built from scratch using Next.js 14. Although Tailwind CSS and DaisyUI are used for styling, the site also includes custom UI elements and components. Localization is handled both on the [server](/i18n/server.ts) and [client](/i18n/client.ts) sides[\*](/i18n/settings.ts), without relying on routing, by configuring i18n on the server to detect and set the language based on the user's browser or referrer information. Third-party services like Cloudinary, Supabase, and Vercel Analytics / SpeedInsight have been integrated, ensuring that Vercel Analytics or SpeedInsight are not activated without user consent, and no personal information is collected without permission.
This project began with the goal of creating a personal website to showcase my skills and portfolio. I have chosen "CrunchyPix" as the website and brand name. This website supports multiple languages through i18n and was built from scratch using Next.js 14. Although Tailwind CSS and DaisyUI are used for styling, the site also includes custom UI elements and components. Localization is handled both on the [server](/i18n/server.ts) and [client](/i18n/client.ts) sides[\*](/i18n/settings.ts). Third-party services like Cloudinary, Supabase, and Vercel Analytics / SpeedInsight have been integrated, ensuring that Vercel Analytics or SpeedInsight are not activated without user consent, and no personal information is collected without permission.

## 📦 Technology Stack

Expand Down Expand Up @@ -40,7 +40,6 @@ This project began with the goal of creating a personal website to showcase my s

- [Nodemailer](https://nodemailer.com) - Email sending library.
- [Country-Flag-Icons](https://github.com/catamphetamine/country-flag-icons) - Collection of country flag icons.
- [Cookies-next](https://github.com/cookie-notice/cookies-next) - Utility for handling cookies in Next.js applications.

### Third-party Services

Expand Down
5 changes: 5 additions & 0 deletions app/[lang]/[...not_found]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation';

export default function NotFoundCatchAll() {
notFound();
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
21 changes: 16 additions & 5 deletions app/layout.tsx → app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ import { ArrowToTop } from '@/components/Buttons/ArrowToTop';
import AllRoutes from '@/components/RooteTitles/AllRoutes';
import CookieConsent from '@/components/Cookies/CookiesConsent';
import Cookies from '@/components/Cookies/Cookies';
import { getLocale } from '@/i18n/server';
import { generatePageMetadata } from '../lib/metadata';
import { generatePageMetadata } from '../../lib/metadata';
import PortfolioDataStore from '@/components/PortfolioDataStore';
import { Locales, supportedLocales } from '@/i18n/settings';
import { dir } from 'i18next';

const inter = Inter({ subsets: ['latin'] });
export async function generateMetadata(): Promise<Metadata> {
return generatePageMetadata('home');
}

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
export function generateStaticParams() {
return supportedLocales.map((lang) => ({ lang }));
}

const RootLayout = async ({
children,
params: { lang },
}: {
children: React.ReactNode;
params: { lang: Locales };
}) => {
return (
<html lang={getLocale()}>
<html lang={lang} dir={dir(lang)}>
<body className="lg:overflow-x-hidden">
<AppI18nProvider>
<AppI18nProvider lang={lang}>
<AppReduxProvider>
<CustomCursor />
<CookieConsent />
Expand Down
2 changes: 1 addition & 1 deletion app/not-found.tsx → app/[lang]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';
import { useTranslation } from '@/hooks/useTranslation';
import { CldImage } from 'next-cloudinary';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';

const Custom404 = () => {
const router = useRouter();
Expand Down
21 changes: 16 additions & 5 deletions app/page.tsx → app/[lang]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import 'swiper/css/navigation';
import { sectionsData } from '@/constants/sections';
import { SectionsSchema, SectionsTypes } from '@/schemas';
import useSupabaseFetch from '@/hooks/useSupabaseFetch';
import { memo, useEffect } from 'react';
import { memo, useEffect, useState } from 'react';
import filterByLanguage from '@/lib/utils/filterByLanguage';
import { getLocale } from '@/i18n/client';
import { useDispatch, useSelector } from 'react-redux';
import { setSectionItems } from '@/store/redux/sectionItems';
import { RootState } from '@/store';
import FsLoading from '@/components/Loading/FsLoading';
import { Locales } from '@/i18n/settings';

const Section = dynamic(() => import('@/components/Sections/Section'), {
ssr: false,
loading: () => <FsLoading />,
});

const Home = () => {
const Home = ({ params: { lang } }: { params: { lang: Locales } }) => {
const dispatch = useDispatch();
const filteredData = useSelector((state: RootState) => state.section.items);
const { data, loading, error } = useSupabaseFetch<SectionsTypes>(
Expand All @@ -29,9 +29,19 @@ const Home = () => {
`*, translations(*, cards(*))`,
SectionsSchema,
);
const language = getLocale();

const [language, setLanguage] = useState<string | null>(null);

useEffect(() => {
const fetchLanguage = () => {
setLanguage(lang);
};

fetchLanguage();
}, [lang]);

useEffect(() => {
if (data && !loading) {
if (language && data && !loading) {
const filteredItems = filterByLanguage({
items: data,
language,
Expand All @@ -44,6 +54,7 @@ const Home = () => {
if (error) {
console.error(error);
}

return !filteredData || filteredData.length <= 1 || error || loading ? (
<FsLoading />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,28 @@ const PolicyCreator = ({ id }: { id: string }) => {
);

const [filteredData, setFilteredData] = useState<PoliciesTypes[]>([]);
const language = getLocale() || i18next.language;
const [language, setLanguage] = useState<string | null>(null);

useEffect(() => {
if (data) {
const fetchLanguage = async () => {
const locale = await getLocale();
setLanguage(locale || i18next.language);
};

fetchLanguage();
}, []);

useEffect(() => {
if (data && language) {
const filteredDat = filterByLanguage({
items: data,
language,
localPath: 'translations',
});
setFilteredData(filteredDat);
}
}, [data, language, setFilteredData]);
console.log(filteredData);
}, [data, language]);

useEffect(() => {
document.title = t('meta.title');
}, [t, language]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import PolicyCreator from '@/app/policies/[id]/components/PolicyCreator';
import PolicyCreator from '@/app/[lang]/policies/[id]/components/PolicyCreator';
import { fetchSupabaseData } from '@/lib/utils/fetchSupabaseData';
import { PoliciesTypes, PoliciesSchema } from '@/schemas';
import { notFound } from 'next/navigation';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const OtherProjects = () => {
);
useEffect(() => {
const urlParts = pathname.split('/');
const currentChildPage = urlParts[2];
const currentChildPage = urlParts[3];
if (!currentChildPage || currentChildPage === '') {
setShow(false);
} else {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
26 changes: 26 additions & 0 deletions app/actions/setCookiesConsent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use server';
import { cookies } from 'next/headers';

export async function setCookiesConsent() {
const cookieStore = cookies();

cookieStore.set('cookiesConsent', 'true', {
path: '/',
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
sameSite: 'strict',
secure: true,
httpOnly: true,
});

return {
success: true,
};
}

export async function getCookieConsent(): Promise<string> {
const response = await cookies().get('cookiesConsent');
if (response && response.value === 'true') {
return 'true';
}
return 'false';
}
35 changes: 35 additions & 0 deletions app/actions/switch-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use server';

import { cookies } from 'next/headers';
import { Locales, NEXT_LOCALE, supportedLocales } from '@/i18n/settings';

export async function switchLocaleAction(value: Locales) {
if (supportedLocales.includes(value)) {
cookies().set(NEXT_LOCALE, value, {
path: '/',
maxAge: 365 * 24 * 60 * 60,
httpOnly: false,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
});

return {
status: 'success',
};
}

return {
status: 'error',
message: 'Unsupported locale',
};
}

export async function getLocaleCookie(): Promise<Locales | null> {
const response = cookies().get(NEXT_LOCALE);

if (response && supportedLocales.includes(response.value as Locales)) {
return response.value as Locales;
}

return null;
}
18 changes: 13 additions & 5 deletions app/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/private/', '/public', '/actions/', '/404', '/policies/*'],
},
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/private/*',
'/public/*',
'/actions/*',
'/404',
'/policies/*',
],
},
],
sitemap: 'https://crunchypix.com/sitemap.xml',
};
}
35 changes: 30 additions & 5 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,43 @@ import { fetchSupabaseData } from '@/lib/utils/fetchSupabaseData';
import { PortfolioItemProps, PortfolioItemSchema } from '@/schemas';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://crunchypix.com';
const staticUrls = [
{
url: 'https://crunchypix.com',
url: `${baseUrl}/en`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 1,
alternates: {
languages: {
de: `${baseUrl}/de`,
tr: `${baseUrl}/tr`,
},
},
},
{
url: 'https://crunchypix.com/portfolio',
url: `${baseUrl}/en/portfolio`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.85,
alternates: {
languages: {
de: `${baseUrl}/de/portfolio`,
tr: `${baseUrl}/tr/portfolio`,
},
},
},
{
url: 'https://crunchypix.com/about',
url: `${baseUrl}/en/about`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.5,
alternates: {
languages: {
de: `${baseUrl}/de/about`,
tr: `${baseUrl}/tr/about`,
},
},
},
];

Expand All @@ -32,10 +51,16 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
);

const dynamicUrls = portfolioItems.map((item) => ({
url: `https://crunchypix.com/portfolio/${item._id}`,
url: `${baseUrl}/en/portfolio/${item._id}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
priority: 0.75,
alternates: {
languages: {
de: `${baseUrl}/de/portfolio/${item._id}`,
tr: `${baseUrl}/tr/portfolio/${item._id}`,
},
},
}));

return [...staticUrls, ...dynamicUrls];
Expand Down
34 changes: 10 additions & 24 deletions components/Cookies/Cookies.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
'use client';
import { RootState } from '@/store';
import { getCookieConsent } from '@/app/actions/setCookiesConsent';
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { getCookie, hasCookie } from 'cookies-next';
import React, { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';

const Cookies = () => {
const cookiesConsent = useSelector(
(state: RootState) => state.cookieConsent.cookieConsent,
);
const beforeSendHandler = useCallback((e: any) => {
if (hasCookie('cookiesConsent') && getCookie('cookiesConsent') === 'true') {
return e;
}
return null;
}, []);

useEffect(() => {
beforeSendHandler(undefined);
}, [beforeSendHandler]);
export default async function Layout() {
const cookieConsent = await getCookieConsent();

return (
<>
<Analytics mode="auto" beforeSend={beforeSendHandler} />
<SpeedInsights beforeSend={beforeSendHandler} />
{cookieConsent === 'true' ? (
<>
<Analytics mode="auto" />
<SpeedInsights />
</>
) : null}
</>
);
};

export default Cookies;
}
Loading

1 comment on commit e1c4286

@vercel
Copy link

@vercel vercel bot commented on e1c4286 Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.