commit 68b7405c52615ef5cdf4dca99284e3c56faaaeda Author: Julien Aldon Date: Mon Jan 19 11:43:59 2026 +0100 Import repositories from gitlab diff --git a/next/.dockerignore b/next/.dockerignore new file mode 100644 index 0000000..9c755d1 --- /dev/null +++ b/next/.dockerignore @@ -0,0 +1,15 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +.git +/out/ +/build +.DS_Store +yarn-debug.log* +yarn-error.log* +.vercel +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/next/.env.example b/next/.env.example new file mode 100644 index 0000000..51f0820 --- /dev/null +++ b/next/.env.example @@ -0,0 +1,5 @@ +NEXT_PUBLIC_CONTENT_URI=http://127.0.0.1:1337/api +NEXT_PUBLIC_IMG_URI=http://127.0.0.1:1337 +NEXT_PUBLIC_ORIGIN=http://localhost:3000 +NEXT_PRIVATE_CONTENT_URI=https://fefan-backend:1337/api +NEXT_PRIVATE_IMG_URI=https://fefan-backend:1337 \ No newline at end of file diff --git a/next/.eslintrc.json b/next/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/next/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/next/.gitignore b/next/.gitignore new file mode 100644 index 0000000..b9f8d23 --- /dev/null +++ b/next/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.env \ No newline at end of file diff --git a/next/Dockerfile b/next/Dockerfile new file mode 100644 index 0000000..77fa257 --- /dev/null +++ b/next/Dockerfile @@ -0,0 +1,32 @@ +FROM node:20.10-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json yarn.lock ./ +RUN yarn --frozen-lockfile + +# Copy source for runtime build +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build at container startup instead of build time +RUN mkdir .next && chown nextjs:nodejs .next +USER nextjs + +EXPOSE 3000 +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +# Build and start in one step +CMD ["sh", "-c", "yarn build && yarn start"] \ No newline at end of file diff --git a/next/README.md b/next/README.md new file mode 100644 index 0000000..0dc9ea2 --- /dev/null +++ b/next/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/next/api/index.js b/next/api/index.js new file mode 100644 index 0000000..0f56f1f --- /dev/null +++ b/next/api/index.js @@ -0,0 +1,19 @@ +import qs from "qs"; + +async function getData(path, query) { + const queryString = qs.stringify(query); + const isServerSide = typeof window === "undefined"; + const res = await fetch( + `${ + isServerSide + ? process.env.NEXT_PRIVATE_CONTENT_URI ?? + "http://fefan-backend:1337/api" + : process.env.NEXT_PUBLIC_CONTENT_URI ?? "https://content.fefan.fr/api" + }/${path}?${queryString}`, + { cache: "no-store" } + ); + + return await res.json(); +} + +export default getData; diff --git a/next/app/contact/page.js b/next/app/contact/page.js new file mode 100644 index 0000000..b5d681f --- /dev/null +++ b/next/app/contact/page.js @@ -0,0 +1,24 @@ +import getData from "@/api"; +import { BlocksRenderer } from "@strapi/blocks-react-renderer"; +import styles from "./style.module.scss"; +import Email from "@/components/email"; +export const dynamic = "force-dynamic"; + +export async function generateMetadata() { + return { + metadataBase: `${process.env.NEXT_PUBLIC_ORIGIN}`, + title: "Contactez nous !", + }; +} + +export default async function Contact() { + const site = await getData("site", {}); + + const content = site.data?.attributes.contact_text; + return ( +
+
{content ? : null}
+ {site.data?.attributes.contact_mail ? : null} +
+ ); +} diff --git a/next/app/contact/style.module.scss b/next/app/contact/style.module.scss new file mode 100644 index 0000000..caa3979 --- /dev/null +++ b/next/app/contact/style.module.scss @@ -0,0 +1,37 @@ +.main { + display: flex; + flex: 1 1 0%; + flex-flow: column; + align-items: center; + + article { + width: 60rem; + display: flex; + flex-flow: column; + max-width: 90vw; + align-self: center; + + h2, h3, h4 { + font-family: var(--font-details); + } + + h2 { + align-self: center; + font-family: var(--font-details); + font-size: 2rem; + font-weight: 400; + } + + p { + margin: 10px; + } + } +} + +@media only screen and (max-width: 641px) { + .main { + article { + text-align: justify; + } + } +} \ No newline at end of file diff --git a/next/app/editions/[editionId]/page.js b/next/app/editions/[editionId]/page.js new file mode 100644 index 0000000..a7a0b24 --- /dev/null +++ b/next/app/editions/[editionId]/page.js @@ -0,0 +1,216 @@ +import getData from "@/api"; +import styles from "./style.module.scss"; +import EditionElement from "@/components/editionElement"; +import EditionGallery from "@/components/editionGallery"; +import PressBlock from "@/components/pressBlock"; +import Empty from "@/components/empty"; +import VideoBlock from "@/components/videoBlock"; +import Fanfare from "@/components/fanfare"; +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ params }) { + const data = await getData(`editions/${params.editionId}`, { + populate: { + flyer: { + fields: ["name", "alternativeText", "caption", "url"], + }, + }, + filters: { + $or: [{ id: { $eq: params.editionId } }], + }, + }); + const activeEditionData = await getData("site", { + populate: { + edition: { + fields: ["id"], + }, + }, + fields: ["author"], + }); + const activeEdition = activeEditionData?.data?.attributes.edition.data.id; + const edition = data.data?.attributes.publishedAt ? data : null; + const flyer = edition?.data?.attributes.flyer.data.attributes; + + return edition + ? { + metadataBase: `${process.env.NEXT_PUBLIC_ORIGIN}`, + title: edition.title, + description: edition.description, + alternates: { + canonical: + data.data.id !== activeEdition + ? `${process.env.NEXT_PUBLIC_ORIGIN}/editions/${params.editionId}` + : `${process.env.NEXT_PUBLIC_ORIGIN}`, + }, + openGraph: { + title: edition.title, + url: + data.data.id !== activeEdition + ? `${process.env.NEXT_PUBLIC_ORIGIN}/editions/${params.editionId}` + : `${process.env.NEXT_PUBLIC_ORIGIN}`, + description: edition.description, + images: { + url: `${process.env.NEXT_PUBLIC_IMG_URI}${flyer.url}`, + width: flyer.width, + height: flyer.height, + }, + authors: [activeEditionData.data.attributes.author], + type: "website", + locale: "fr_FR", + siteName: "Le Fefan - Festival de Fanfares", + }, + } + : {}; +} + +export default async function Edition({ params }) { + const data = await getData(`editions/${params.editionId}`, { + populate: { + flyer: { + fields: ["name", "alternativeText", "caption", "url"], + }, + gallery: { + fields: ["name", "alternativeText", "caption", "url"], + }, + programs: { + fields: ["map_uri", "introduction", "description", "title", "type"], + populate: { + bands: { + fields: ["name", "location"], + }, + }, + }, + statistics: { + fields: ["name", "value", "publishedAt"], + }, + social_links: { + fields: ["uri", "type"], + }, + articles: { + fields: ["title", "link", "excerpt", "publishedAt"], + }, + fields: ["movie"], + }, + filters: { + $or: [{ id: { $eq: params.editionId } }], + }, + }); + const edition = data.data?.attributes.publishedAt ? data : null; + const flyer = edition?.data?.attributes.flyer.data.attributes; + const gallery = edition?.data?.attributes.gallery.data; + const statistics = edition?.data?.attributes.statistics.data; + const allArticles = edition?.data?.attributes.articles.data; + const articles = allArticles + ? allArticles.filter((el) => el.attributes.publishedAt != null) + : []; + const movie = edition.data.attributes.movie + ? edition.data.attributes.movie + : null; + const programs = edition?.data?.attributes?.programs?.data + ? edition.data.attributes.programs.data + : []; + const program = + programs.filter((el) => el.attributes.type === "city-wide")[0] ?? null; + const bands = program ? program?.attributes?.bands.data : []; + + return ( +
+ {edition ? ( + <> +

{edition.data.attributes.title}

+

{edition.data.attributes.subtitle}

+ ({ + id, + type: "stat", + title: attributes.name, + value: attributes.value, + }))} + /> + {movie && bands ? ( +
+ {bands.length > 0 ? ( +
+

Les fanfares

+ {bands.map(({ id, attributes: attr }) => { + return ( + + ); + })} +
+ ) : null} + {movie ? ( + <> + + + ) : null} +
+ ) : null} + {articles.length > 0 ? ( + <> +

Ils ont parlé de nous !

+ {articles.map(({ id, attributes }, index) => { + const offset = index + (articles.length - gallery.length) / 2; + return index < gallery.length ? ( + + ) : ( + + ); + })} + + ) : null} + {articles.length <= gallery.length ? ( + <> +

Les photos du Fefan

+ + + ) : null} + + ) : ( + + )} +
+ ); +} diff --git a/next/app/editions/[editionId]/style.module.scss b/next/app/editions/[editionId]/style.module.scss new file mode 100644 index 0000000..9240efa --- /dev/null +++ b/next/app/editions/[editionId]/style.module.scss @@ -0,0 +1,64 @@ +.main { + flex: 1 1 0%; + display: flex; + align-items: center; + flex-flow: column; + padding-top: 1rem; + + h2 { + font-family: var(--font-details); + font-size: 2rem; + font-weight: 400; + margin: 0; + } + + h3 { + font-size: 0.75rem; + color: var(--fg-1); + font-weight: 700; + text-transform: uppercase; + margin: 0; + margin-bottom: 1rem; + } + + h4 { + font-family: var(--font-details); + font-size: 1.8rem; + font-weight: 400; + } + + .smallProgram { + margin-top: 2rem; + display: flex; + + .featuring { + + h4 { + grid-column: 1 / -1; + } + display: grid; + align-self: center; + grid-template-columns: repeat(2, calc(80rem / 6)); + align-self: stretch; + padding: 0 0.5rem; + justify-self: stretch; + align-items: stretch; + justify-items: stretch; + align-content: start; + grid-auto-rows: max-content; + } + } +} + +@media (max-width: 60rem) { + .main { + .smallProgram { + flex-direction: column; + + .featuring { + grid-template-columns: 1fr; + align-self: center; + } + } + } +} \ No newline at end of file diff --git a/next/app/editions/page.js b/next/app/editions/page.js new file mode 100644 index 0000000..78abd54 --- /dev/null +++ b/next/app/editions/page.js @@ -0,0 +1,132 @@ +import getData from "@/api"; +import styles from "./style.module.scss"; +import EditionElement from "@/components/editionElement"; +import Empty from "@/components/empty"; +export const dynamic = "force-dynamic"; + +export async function generateMetadata() { + const site = await getData("site", { + fields: ["description", "author"], + }); + + const data = await getData("editions", { + populate: { + gallery: { + fields: ["name", "url", "alternativeText"], + }, + statistics: { + fields: ["name", "value"], + }, + flyer: { + fields: ["name", "url", "alternativeText", "width", "height"], + }, + }, + }); + const editions = data.data; + + return site.data + ? { + metadataBase: `${process.env.NEXT_PUBLIC_ORIGIN}`, + title: "Editions précédentes — Le Fefan", + description: site.data.attributes.description, + alternates: { + canonical: "/editions", + }, + openGraph: { + title: "Editions précédentes — Le Fefan", + url: `${process.env.NEXT_PUBLIC_ORIGIN}/prog/city-wide`, + description: site.data.attributes.description, + images: editions.map(({ attributes: attr }) => ({ + width: attr.flyer.data.attributes.width, + height: attr.flyer.data.attributes.height, + alt: attr.flyer.data.attributes.alternativeText, + url: attr.flyer.data.attributes.url, + })), + authors: [site.data.attributes.author], + type: "website", + locale: "fr_FR", + siteName: "Le Fefan - Festival de Fanfares", + }, + } + : {}; +} + +export default async function Editions() { + const site = await getData("site", { + fields: ["id"], + populate: { + edition: { + fields: ["id"], + }, + }, + }); + + const data = await getData("editions", { + populate: { + gallery: { + fields: ["name", "url", "alternativeText"], + }, + statistics: { + fields: ["name", "value"], + }, + flyer: { + fields: ["name", "url", "alternativeText"], + }, + fields: ["movie"], + }, + }); + + const editions = (data.data ?? []).filter( + (e) => e.id !== site.data?.attributes.edition.data?.id + ); + + return ( +
+ {editions ? ( + editions.map(({ id, attributes: attr }) => { + const stats = attr.statistics.data.map(({ id, attributes }) => ({ + id, + type: "stat", + title: attributes.name, + value: attributes.value, + })); + const images = attr.gallery.data.map(({ id, attributes }) => ({ + id, + type: "image", + title: attributes.alternativeText, + value: `${process.env.NEXT_PUBLIC_IMG_URI}${attributes.url}`, + })); + const movie = attr.movie + ? [ + { + id: "movie", + type: "video", + title: `Aftermovie ${attr.title}`, + value: attr.movie, + mode: "thumbnail", + }, + ] + : null; + + return ( + + ); + }) + ) : ( + + )} +
+ ); +} diff --git a/next/app/editions/style.module.scss b/next/app/editions/style.module.scss new file mode 100644 index 0000000..975aab8 --- /dev/null +++ b/next/app/editions/style.module.scss @@ -0,0 +1,12 @@ +.main { + flex: 1 1 0%; + display: flex; + flex-flow: column; + align-items: center; + height: 100vh; + overflow: auto; + scroll-snap-type: y mandatory; + + // .editionLink { + // } +} \ No newline at end of file diff --git a/next/app/favicon.ico b/next/app/favicon.ico new file mode 100644 index 0000000..16c95e2 Binary files /dev/null and b/next/app/favicon.ico differ diff --git a/next/app/globals.css b/next/app/globals.css new file mode 100644 index 0000000..2c368d4 --- /dev/null +++ b/next/app/globals.css @@ -0,0 +1,44 @@ +:root { + --bg-0: #FFFFCC; + --fg-0: #412D0A; + --fg-0-translucid: rgba(65, 45, 10, 0.35); + --fg-1: #957948; + --fg-2: #CCC08F; + --fg-3: #DDDDAA; + --fefan-1: #1B519E; + --fefan-2: #295DAB; + --fefan-3: #32A1DA; + --fefan-4: #B8529F; + --transition-time: 0.2s; + --font-body: 'Krub', sans-serif; + --font-details: 'Shadows Into Light', sans-serif; + --border-width: 1px; +} + +html, body { + background: var(--bg-0); + color: var(--fg-0); + height: 100vh; + margin: 0; + font-family: var(--font-body); + display: flex; + flex-direction: column; +} + +a:not(.not-a-link) { + text-decoration-color: transparent; + color: transparent; + background: linear-gradient(175deg, var(--fefan-1) 0%, var(--fefan-1) 45%, var(--fefan-2) 55%, var(--fefan-3) 70%, var(--fefan-4) 85%); + background-clip: text; + background-size: 250% 100%; + transition: all ease var(--transition-time); + + &:hover { + text-decoration-color: var(--fefan-1); + background-position-x: 100%; + } + + &:visited { + color: unset; + } +} \ No newline at end of file diff --git a/next/app/layout.js b/next/app/layout.js new file mode 100644 index 0000000..d4e93b2 --- /dev/null +++ b/next/app/layout.js @@ -0,0 +1,28 @@ +import './globals.css' +import Header from '@/components/header' +import Footer from '@/components/footer' + +export const dynqmic = 'force-dynamic' + +export const metadata = { + generator: 'Next.js', + applicationName: 'Le Fefan', + keywords: ['Fefan', 'Festival', 'Fanfare', 'Cuivres', 'Villeurbanne', 'Evenement', 'Musique'], + authors: [{ name: 'Girasol' }], + creator: 'Girasol', +} + +export default function RootLayout({ children }) { + return ( + + + + + +
+ {children} +