├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .husky └── pre-commit ├── README.md ├── app ├── [...not_found] │ └── page.tsx ├── api │ └── subscribe │ │ └── route.ts ├── authors │ ├── [author] │ │ └── page.tsx │ ├── loading.tsx │ └── page.tsx ├── favicon.ico ├── functions │ ├── formatString.ts │ ├── getArticles.ts │ ├── getNews.ts │ └── getPodcasts.ts ├── globals.css ├── layout.tsx ├── magazine │ ├── [title] │ │ └── page.tsx │ └── page.tsx ├── not-found.tsx ├── page.tsx ├── podcasts │ ├── [title] │ │ ├── loading.tsx │ │ └── page.tsx │ └── page.tsx ├── robots.ts └── sitemap.xml ├── components.json ├── components ├── ArticleFilterButtons.tsx ├── Articles │ ├── Articles.tsx │ └── loading.tsx ├── AuthorList.tsx ├── Authors │ ├── Authors.tsx │ └── loading.tsx ├── Footer.tsx ├── FooterSocialsLinks.tsx ├── Header.tsx ├── Hero.tsx ├── LatestArticles.tsx ├── LatestArticles │ ├── LatestArticles.tsx │ ├── SkeletonCard.tsx │ └── loading.tsx ├── LatestPodcasts │ ├── LatestPodcasts.tsx │ ├── SkeletonCard.tsx │ └── loading.tsx ├── NewsTicker │ ├── NewsTicker.tsx │ └── loading.tsx ├── NewsletterSignUp.tsx ├── NewsletterTicker.tsx ├── PageTitle.tsx ├── PodcastsList │ ├── PodcastCard.tsx │ ├── PodcastsList.tsx │ └── loading.tsx ├── PopularArticles.tsx ├── PostNavigation.tsx ├── Sidebar.tsx ├── SocialSharing.tsx ├── Subheading.tsx └── ui │ ├── button.tsx │ ├── container.tsx │ ├── input.tsx │ ├── separator.tsx │ ├── sheet.tsx │ └── skeleton.tsx ├── context ├── ArticleContext.tsx └── PodcastContext.tsx ├── data └── menu.ts ├── e2e ├── a11y.spec.ts └── navigation.spec.ts ├── hooks ├── useArticleContext.tsx └── usePodcastContext.tsx ├── json ├── articles.json ├── news.json └── podcasts.json ├── lib ├── types.ts └── utils.ts ├── next.config.js ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── public ├── fonts │ ├── general-sans │ │ ├── GeneralSans-Medium.woff2 │ │ ├── GeneralSans-Regular.woff2 │ │ ├── GeneralSans-Semibold.woff2 │ │ └── License │ │ │ └── FFL.txt │ └── satoshi │ │ ├── License │ │ └── FFL.txt │ │ └── Satoshi-Regular.woff2 ├── icons │ ├── line.svg │ ├── menu.svg │ ├── ri_apple-fill.svg │ ├── ri_arrow-right-line.svg │ ├── ri_instagram-line-white.svg │ ├── ri_instagram-line.svg │ ├── ri_rss-fill-white.svg │ ├── ri_rss-fill.svg │ ├── ri_soundcloud-line.svg │ ├── ri_spotify-fill.svg │ ├── ri_twitter-fill-white.svg │ ├── ri_twitter-fill.svg │ ├── ri_youtube-fill-white.svg │ └── ri_youtube-fill.svg ├── images │ ├── articles │ │ ├── preview │ │ │ ├── an-indestructible-hope.jpg │ │ │ ├── artists-who-want-to-rise-above.jpg │ │ │ ├── beauty-of-colors.jpg │ │ │ ├── colorful-future.jpg │ │ │ ├── dont-close-your-eyes.jpg │ │ │ ├── getting-real.jpg │ │ │ ├── history-of-paper.jpg │ │ │ ├── hope-dies-last.jpg │ │ │ ├── how-are-you-really.jpg │ │ │ ├── keep-on-smiling.jpg │ │ │ ├── most-colorful-places.jpg │ │ │ ├── only-in-your-heart.jpg │ │ │ ├── secret-garden.jpg │ │ │ ├── street-art-festival.jpg │ │ │ ├── the-best-art-museums.jpg │ │ │ ├── the-chains-of-our-lives.jpg │ │ │ ├── the-devil-is-the-details.jpg │ │ │ └── through-the-eyes-of-street-artists.jpg │ │ └── single-post │ │ │ ├── an-indestructible-hope.jpg │ │ │ ├── artists-who-want-to-rise-above.jpg │ │ │ ├── beauty-of-colors.jpg │ │ │ ├── colorful-future.jpg │ │ │ ├── dont-close-your-eyes.jpg │ │ │ ├── getting-real.jpg │ │ │ ├── history-of-paper.jpg │ │ │ ├── hope-dies-last.jpg │ │ │ ├── how-are-you-really.jpg │ │ │ ├── keep-on-smiling.jpg │ │ │ ├── most-colorful-places.jpg │ │ │ ├── only-in-your-heart.jpg │ │ │ ├── secret-garden.jpg │ │ │ ├── street-art-festival.jpg │ │ │ ├── the-best-art-museums.jpg │ │ │ ├── the-chains-of-our-lives.jpg │ │ │ ├── the-devil-is-the-details.jpg │ │ │ └── through-the-eyes-of-street-artists.jpg │ ├── authors │ │ ├── anna-nielsen.jpg │ │ ├── anne-henry.jpg │ │ ├── bekah-allmark-Qt0ogPnhGWY-unsplash 1.jpg │ │ ├── cristofer-vaccaro.jpg │ │ ├── emiliano-vittoriosi-fIM5oAdHAxE-unsplash 1.jpg │ │ ├── jack-finnigan-rriAI0nhcbc-unsplash 1.jpg │ │ ├── jakob-grønberg.jpg │ │ ├── jane-cooper.jpg │ │ ├── jeffery-erhunse-vp9mRauo68c-unsplash 1.jpg │ │ ├── louise-jensen.jpg │ │ ├── metin-ozer-iSmTwuKTNDo-unsplash 1.jpg │ │ └── olena-sergienko-0TSd6uCKTKc-unsplash 1.jpg │ ├── homepage │ │ └── magazine-cover.jpg │ ├── podcasts │ │ └── preview │ │ │ ├── podcast-cover-ep01.jpg │ │ │ ├── podcast-cover-ep02.jpg │ │ │ ├── podcast-cover-ep03.jpg │ │ │ ├── podcast-cover-ep04.jpg │ │ │ └── podcast-cover-ep05.jpg │ └── titles │ │ ├── Art&Life.svg │ │ ├── Authors.svg │ │ ├── Magazine.svg │ │ ├── NotFound.svg │ │ └── Podcast.svg └── logos │ ├── FyrreMagazineFavicon.svg │ ├── FyrreMagazineLogo-Black.svg │ └── FyrreMagazineLogo-White.svg ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { "@next/next/no-img-element": "off" } 4 | } 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | polar: asbhogal 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run test:e2e 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Fyrre - Art & Life Magazine Website

4 | 5 |
6 | 7 | A bold, striking arts and life magazine website build as an SPA from Figma templates (designed by Webflow designer Pawel Gola), using Next.js, React Server Components, React Suspense, TypeScript, Tailwind, Shadcn and ES Lint and incorporates E2E testing using Playwright. 8 | 9 | Features 10 | - Faithful adaptation to Figma designs 11 | - Custom designed and developed 404 error page 12 | - Dynamic rendering of podcast, article and author data 13 | - Dynamic filtering of magazine articles based on categories 14 | - JSON data created for articles, podcast and authors to emulate API endpoints 15 | - React Suspense for UI loading states until async fetched content is available 16 | - React Server Components (app router pages) 17 | - TypeScript to enforce type safety 18 | - React Context API to store data fetched at top level 19 | - Custom hooks for podcast and article context store calls 20 | - GSAP animations for horizontal sliding text 21 | - Shadcn for accessible components 22 | - Tailwind CSS for mobile-first responsiveness 23 | - E2E testing across multiple browsers using Playwright 24 | - Husky to run lint and testing prior to Git Commit 25 | - React Hook form with Zod Schema validation for email subscription input 26 | - Server Actions and Errors using Next.js api routes for server-side validation 27 | 28 | Stacks & Tools 29 |
30 |
31 | nextjs logo 32 | react logo 33 | TypeScript logo 34 | Tailwind logo 35 | Shadcn logo 36 | ES Lint logo 37 | GSAP logo 38 | Playwright logo 39 | React Hook Form logo 40 | Zod logo 41 | 42 | Links 43 | - Fyrre Magazine Site 44 | - Fyrre Magazine Templates 45 | 46 | Disclaimer 47 | 48 | As far as the developer is aware all the individuals mentioned in this website are purely fictionalized. Any resemblance to individuals or entities, living or dead, is entirely coincidental and the developer bears no responsibility for any such resemblance. 49 | -------------------------------------------------------------------------------- /app/[...not_found]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | export default function NotFoundCatchAll() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /app/api/subscribe/route.ts: -------------------------------------------------------------------------------- 1 | import { subscribeNewsletterSchema } from "@/lib/types"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function POST(request: Request) { 5 | const body: unknown = await request.json(); 6 | 7 | const result = subscribeNewsletterSchema.safeParse(body); 8 | 9 | let zodErrors = {}; 10 | if (!result.success) { 11 | result.error.issues.forEach((issue) => { 12 | zodErrors = { ...zodErrors, [issue.path[0]]: issue.message }; 13 | }); 14 | } 15 | 16 | return NextResponse.json( 17 | Object.keys(zodErrors).length > 0 18 | ? { errors: zodErrors } 19 | : { success: true } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/authors/[author]/page.tsx: -------------------------------------------------------------------------------- 1 | import formatString from "@/app/functions/formatString"; 2 | import { getArticles } from "@/app/functions/getArticles"; 3 | import PostNavigation from "@/components/PostNavigation"; 4 | import SocialSharing from "@/components/SocialSharing"; 5 | import Link from "next/link"; 6 | 7 | type AuthorData = { 8 | author: string; 9 | job: string; 10 | city: string; 11 | avatar: string; 12 | imgAlt: string; 13 | slug: string; 14 | biography: { 15 | summary: string; 16 | body: string; 17 | }; 18 | articles: ArticleData[]; 19 | }; 20 | 21 | type ArticleData = { 22 | title: string; 23 | img: string; 24 | date: string; 25 | read: string; 26 | label: string; 27 | slug: string; 28 | }; 29 | 30 | export async function generateMetadata({ 31 | params, 32 | }: { 33 | params: { author: string }; 34 | }) { 35 | const authors: AuthorData[] = await getArticles(); 36 | 37 | const decodedAuthor = decodeURIComponent(params.author); 38 | 39 | const authorData = authors.find( 40 | (author: AuthorData) => author.slug === decodedAuthor 41 | ); 42 | 43 | if (!authorData) { 44 | return { 45 | title: "Author Not Found", 46 | }; 47 | } 48 | 49 | return { 50 | title: `${authorData.author} | Fyrre Magazine`, 51 | }; 52 | } 53 | 54 | export default async function AuthorDetails({ 55 | params, 56 | }: { 57 | params: { author: string }; 58 | }) { 59 | try { 60 | const authors: AuthorData[] = await getArticles(); 61 | 62 | const decodedAuthor = decodeURIComponent(params.author); 63 | 64 | const authorData = authors.find( 65 | (author: AuthorData) => author.slug === decodedAuthor 66 | ); 67 | 68 | if (!authorData) { 69 | return

Author not found

; 70 | } 71 | 72 | return ( 73 |
74 | Author 75 |
76 |
77 | {authorData.imgAlt} 78 |
79 |

Follow

80 | 102 |
103 |
104 |
105 |

{authorData.author}

106 |

107 | {authorData.biography.summary} 108 |

109 |

{authorData.biography.body}

110 |
111 |
112 |
113 |

114 | Articles by {authorData.author} 115 |

116 | 117 |
118 |
119 | ); 120 | } catch (error) { 121 | console.error("Error fetching author details:", error); 122 | return

Error fetching author details

; 123 | } 124 | } 125 | 126 | function AuthorArticles({ articles }: { articles: ArticleData[] }) { 127 | return ( 128 |
129 | {articles.map((article, index) => ( 130 |
134 | 135 | {article.title} 140 | 141 |
142 |

143 | {article.title} 144 |

145 |
146 | 147 |

Date

148 | 149 |
150 | 151 |

City

152 |

{article.label}

153 |
154 |
155 |
156 |
157 | ))} 158 |
159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /app/authors/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 |
89 | 90 |
91 |
92 | 93 | 94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 |
113 | 114 | 115 |
116 |
117 |
118 | 119 | 120 |
121 |
122 | 123 | 124 |
125 |
126 | 127 | 128 |
129 |
130 |
131 |
132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /app/authors/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthorsList from "@/components/AuthorList"; 2 | import PageTitle from "@/components/PageTitle"; 3 | import { Suspense } from "react"; 4 | import Loading from "./loading"; 5 | 6 | export const metadata = { 7 | title: "Authors | Fyrre Magazine", 8 | description: "Our authors", 9 | }; 10 | 11 | export default function AuthorsPage() { 12 | return ( 13 |
14 | 19 | Authors 20 | 21 | }> 22 | 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asbhogal/Fyrre-Magazine/c5755215bb675691bb1cd23566cd533617fb833c/app/favicon.ico -------------------------------------------------------------------------------- /app/functions/formatString.ts: -------------------------------------------------------------------------------- 1 | export default function formatString(string: string) { 2 | return string 3 | .replace(/[^a-zA-Z0-9\sø]/g, "") 4 | .replace(/\s+/g, "-") 5 | .toLowerCase(); 6 | } 7 | -------------------------------------------------------------------------------- /app/functions/getArticles.ts: -------------------------------------------------------------------------------- 1 | export type ArticleType = { 2 | id: number; 3 | author: string; 4 | job: string; 5 | city: string; 6 | avatar: string; 7 | imgAlt: string; 8 | slug: string; 9 | articles: Array<{ 10 | title: string; 11 | popular: boolean; 12 | popularity: number; 13 | description: string; 14 | date: string; 15 | read: string; 16 | label: string; 17 | img: string; 18 | imgAlt: string; 19 | slug: string; 20 | content: Array<{ 21 | img: string; 22 | summary: string; 23 | section1: string; 24 | quote: Array; 25 | summary2: string; 26 | section2: string; 27 | }>; 28 | }>; 29 | }; 30 | 31 | export async function getArticles() { 32 | const res = await fetch( 33 | "https://raw.githubusercontent.com/asbhogal/Fyrre-Magazine/main/json/articles.json" 34 | ); 35 | 36 | if (!res.ok) { 37 | throw new Error("Failed to fetch article data"); 38 | } 39 | 40 | return res.json(); 41 | } 42 | -------------------------------------------------------------------------------- /app/functions/getNews.ts: -------------------------------------------------------------------------------- 1 | export async function getNews(): Promise { 2 | const res = await fetch( 3 | "https://raw.githubusercontent.com/asbhogal/Fyrre-Magazine/main/json/news.json" 4 | ); 5 | 6 | if (!res.ok) { 7 | throw new Error("Failed to fetch news data"); 8 | } 9 | 10 | return res.json(); 11 | } 12 | -------------------------------------------------------------------------------- /app/functions/getPodcasts.ts: -------------------------------------------------------------------------------- 1 | export type PodcastType = { 2 | id: number; 3 | title: string; 4 | img: string; 5 | imgAlt: string; 6 | date: string; 7 | duration: string; 8 | episode: string; 9 | slug: string; 10 | content: { 11 | summary: string; 12 | section1: string; 13 | quote: [string, string]; 14 | section2: string; 15 | }[]; 16 | }; 17 | 18 | export async function getPodcasts(): Promise { 19 | const res = await fetch( 20 | "https://raw.githubusercontent.com/asbhogal/Fyrre-Magazine/main/json/podcasts.json" 21 | ); 22 | 23 | if (!res.ok) { 24 | throw new Error("Failed to fetch podcast data"); 25 | } 26 | 27 | return res.json(); 28 | } 29 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 0 0% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | 22 | --muted: 0 0% 96.1%; 23 | --muted-foreground: 0 0% 45.1%; 24 | 25 | --accent: 0 0% 96.1%; 26 | --accent-foreground: 0 0% 9%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 0 0% 89.8%; 32 | --input: 0 0% 89.8%; 33 | --ring: 0 0% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 0 0% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 0 0% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 0 0% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 0 0% 9%; 50 | 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | 57 | --accent: 0 0% 14.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | html { 74 | font-family: General Sans, system-ui, sans-serif; 75 | } 76 | body { 77 | @apply bg-background text-foreground; 78 | } 79 | } 80 | 81 | @layer utilities { 82 | .heading3-title { 83 | font-size: 2rem; 84 | font-weight: 600; 85 | line-height: 2.4rem; 86 | } 87 | .podcast-title { 88 | font-weight: 600; 89 | font-size: clamp(1rem, 0.8997rem + 3.3439vw, 6.25rem); 90 | text-transform: uppercase; 91 | } 92 | .text-blog-subheading { 93 | font-weight: 600; 94 | font-size: clamp(3rem, 2.7857rem + 1.0714vw, 4.5rem); 95 | } 96 | .text-blog-quote { 97 | font-weight: 600; 98 | font-size: clamp(2rem, 1.8571rem + 0.7143vw, 3rem); 99 | } 100 | .text-blog-summary { 101 | font-weight: 500; 102 | font-size: 1.375rem; 103 | } 104 | .text-subheading { 105 | font-weight: 600; 106 | /* font-size: clamp(3.25rem, 2.4853rem + 3.8235vw, 6.5rem); */ 107 | font-size: clamp(2.25rem, 1.6429rem + 3.0357vw, 6.5rem); 108 | text-transform: uppercase; 109 | line-height: 1; 110 | } 111 | .text-subtitle { 112 | line-height: 1; 113 | } 114 | .text-footer-title { 115 | color: #ffffff; 116 | } 117 | .text-subtitle, 118 | .text-footer-title { 119 | font-size: clamp(2.625rem, 1.9917rem + 3.1667vw, 5rem); 120 | font-weight: 600; 121 | text-transform: uppercase; 122 | } 123 | } 124 | 125 | /* * { 126 | outline: 1px solid red; 127 | } */ 128 | 129 | @font-face { 130 | font-family: "General Sans"; 131 | font-weight: 400; 132 | font-style: normal; 133 | src: url("/fonts/general-sans/GeneralSans-Regular.woff2"), format("woff2"); 134 | font-display: swap; 135 | } 136 | 137 | @font-face { 138 | font-family: "General Sans"; 139 | font-weight: 500; 140 | font-style: normal; 141 | src: url("/fonts/general-sans/GeneralSans-Medium.woff2"), format("woff2"); 142 | font-display: swap; 143 | } 144 | 145 | @font-face { 146 | font-family: "General Sans"; 147 | font-weight: 600; 148 | font-style: normal; 149 | src: url("/fonts/general-sans/GeneralSans-Semibold.woff2"), format("woff2"); 150 | font-display: swap; 151 | } 152 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@/components/ui/container"; 2 | import "./globals.css"; 3 | import type { Metadata } from "next"; 4 | import Header from "@/components/Header"; 5 | import Footer from "@/components/Footer"; 6 | import PodcastContextProvider from "@/context/PodcastContext"; 7 | import ArticleContextProvider from "@/context/ArticleContext"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Fyrre Magazine", 11 | description: "Art & Life", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | {children} 34 |