Compare commits

..

No commits in common. "18691d930ebdac57b270d7202b5e8c24aee4002e" and "eb5ae1fea791af2cdde675c971b90b48cd97f4f1" have entirely different histories.

11 changed files with 64 additions and 121 deletions

View file

@ -1,16 +1,6 @@
# Token StoryBlok — Preview token en dev/preview, Public token en production # Token StoryBlok — Preview token en dev/preview, Public token en production
PUBLIC_STORYBLOK_TOKEN= STORYBLOK_TOKEN=
# Mettre à true sur l'instance preview (SSR + visual editor StoryBlok) # Mettre à true sur l'instance preview (SSR + visual editor StoryBlok)
# Ne pas définir ou mettre à false en production (SSG) # Ne pas définir ou mettre à false en production (SSG)
# PUBLIC_STORYBLOK_IS_PREVIEW=true # STORYBLOK_IS_PREVIEW=true
# Webhook rebuild — uniquement sur l'instance preview
# Secret partagé avec StoryBlok (Settings > Webhooks > Secret du Webhook)
# STORYBLOK_WEBHOOK_SECRET=
# Token OAuth Clever Cloud (généré via clever login puis ~/.config/clever-cloud/clever-tools.json)
# CLEVER_TOKEN=
# ID de l'organisation Clever Cloud (orga_xxxxxxxx)
# CLEVER_ORGA_ID=
# ID de l'application Clever Cloud de production (app_xxxxxxxx)
# CLEVER_APP_ID_PRODUCTION=

View file

@ -1,62 +1,20 @@
# Compagnie AspiRêves <div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
Site web de la Compagnie AspiRêves, compagnie de spectacle vivant basée dans le Tarn. # Run and deploy your AI Studio app
Construit avec [Astro](https://astro.build/) et [StoryBlok](https://www.storyblok.com/) comme CMS headless. This contains everything you need to run your app locally.
## Prérequis View your app in AI Studio: https://ai.studio/apps/888a76b0-af1d-4274-9218-1817cdc461fb
- Node.js >= 20 ## Run Locally
- Un fichier `.env` (voir `.env.example`)
## Développement **Prerequisites:** Node.js
```bash
npm install
npm run dev
```
Le serveur démarre sur `http://localhost:3030`. 1. Install dependencies:
`npm install`
## Builds 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
Le projet supporte deux modes de build via la variable `PUBLIC_STORYBLOK_IS_PREVIEW` : `npm run dev`
### Production (SSG)
Site statique, performant, sans serveur Node.js. Utilise le **Public Access Token** StoryBlok et ne récupère que le contenu **publié**.
```bash
npm run build
```
Génère des fichiers HTML statiques dans `dist/`. Le rebuild doit être déclenché par un **webhook StoryBlok** (Settings > Webhooks) à chaque publication de contenu.
### Preview (SSR)
Serveur Node.js avec le **visual editor StoryBlok** (bridge + live preview). Utilise le **Preview Access Token** et récupère le contenu en **draft**.
```bash
PUBLIC_STORYBLOK_IS_PREVIEW=true npm run build
HOST=0.0.0.0 node dist/server/entry.mjs
```
Le serveur démarre sur le port `8080` par défaut (configurable via `PORT`).
### Variables d'environnement
| Variable | Production | Preview |
|---|---|---|
| `PUBLIC_STORYBLOK_TOKEN` | Public Access Token | Preview Access Token |
| `PUBLIC_STORYBLOK_IS_PREVIEW` | *(non défini)* | `true` |
| `STORYBLOK_WEBHOOK_SECRET` | - | Secret du webhook StoryBlok |
| `CLEVER_TOKEN` | - | Token OAuth Clever Cloud |
| `CLEVER_ORGA_ID` | - | ID de l'organisation (orga_xxx) |
| `CLEVER_APP_ID_PRODUCTION` | - | ID de l'app production (app_xxx) |
| `CC_POST_BUILD_HOOK` | `npm run build` | `npm run build` |
| `HOST` | - | `0.0.0.0` |
### Configuration StoryBlok
- **Settings > Visual Editor** : mettre l'URL de l'instance preview comme environnement par défaut
- **Settings > Webhooks** : configurer un webhook `POST` vers `https://<preview-instance>/api/rebuild` pour déclencher le rebuild production à chaque publication

View file

@ -6,8 +6,8 @@ import icon from 'astro-icon';
import node from '@astrojs/node'; import node from '@astrojs/node';
import { storyblok } from '@storyblok/astro'; import { storyblok } from '@storyblok/astro';
const env = loadEnv('', process.cwd(), 'PUBLIC_STORYBLOK'); const env = loadEnv('', process.cwd(), 'STORYBLOK');
const isPreview = env.PUBLIC_STORYBLOK_IS_PREVIEW === 'true'; const isPreview = env.STORYBLOK_IS_PREVIEW === 'true';
export default defineConfig({ export default defineConfig({
output: isPreview ? 'server' : 'static', output: isPreview ? 'server' : 'static',
@ -15,7 +15,7 @@ export default defineConfig({
integrations: [ integrations: [
icon(), icon(),
storyblok({ storyblok({
accessToken: env.PUBLIC_STORYBLOK_TOKEN, accessToken: env.STORYBLOK_TOKEN,
bridge: isPreview, bridge: isPreview,
livePreview: isPreview, livePreview: isPreview,
}), }),

View file

@ -7,7 +7,7 @@
"dev": "astro dev --port 3030 --host 0.0.0.0", "dev": "astro dev --port 3030 --host 0.0.0.0",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"start": "HOST=0.0.0.0 node dist/server/entry.mjs", "start": "node dist/server/entry.mjs",
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"dependencies": { "dependencies": {

View file

@ -26,5 +26,22 @@ const { title = 'Compagnie AspiRêves' } = Astro.props;
<Footer /> <Footer />
<ThemeSwitcher /> <ThemeSwitcher />
<script>
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '-50px' }
);
document.querySelectorAll('.fade-up, .fade-right, .fade-scale').forEach((el) => {
observer.observe(el);
});
</script>
</body> </body>
</html> </html>

View file

@ -2,7 +2,7 @@ import { useStoryblokApi } from '@storyblok/astro';
import type { SbBlokData } from '@storyblok/astro'; import type { SbBlokData } from '@storyblok/astro';
function getVersion(): 'draft' | 'published' { function getVersion(): 'draft' | 'published' {
return import.meta.env.PUBLIC_STORYBLOK_IS_PREVIEW === 'true' ? 'draft' : 'published'; return import.meta.env.STORYBLOK_IS_PREVIEW === 'true' ? 'draft' : 'published';
} }
export interface Spectacle { export interface Spectacle {

1
src/middleware.ts Normal file
View file

@ -0,0 +1 @@
export { onRequest } from '@storyblok/astro/middleware.ts';

View file

@ -17,6 +17,10 @@ const now = new Date();
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<!-- Header --> <!-- Header -->
<div class="animate-entrance text-center mb-12 md:mb-20"> <div class="animate-entrance text-center mb-12 md:mb-20">
<div class="inline-flex items-center gap-2 mb-6 px-4 py-1 rounded-full bg-white/50 border border-white/20 text-dream-coral">
<Icon name="lucide:calendar" size={16} />
<span class="font-sans text-[10px] md:text-xs font-bold uppercase tracking-widest">Calendrier</span>
</div>
<h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8">L'<span class="text-dream-coral italic">Agenda</span></h1> <h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8">L'<span class="text-dream-coral italic">Agenda</span></h1>
<p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed"> <p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed">
Venez nous voir sur scène ! <br class="hidden sm:block" />Chaque date est une nouvelle aventure. Venez nous voir sur scène ! <br class="hidden sm:block" />Chaque date est une nouvelle aventure.

View file

@ -1,36 +0,0 @@
import type { APIRoute } from 'astro';
import { createHmac, timingSafeEqual } from 'node:crypto';
export const POST: APIRoute = async ({ request }) => {
const webhookSecret = import.meta.env.STORYBLOK_WEBHOOK_SECRET;
const token = import.meta.env.CLEVER_TOKEN;
const orgaId = import.meta.env.CLEVER_ORGA_ID;
const appId = import.meta.env.CLEVER_APP_ID_PRODUCTION;
if (!webhookSecret || !token || !orgaId || !appId) {
return new Response('Missing server configuration', { status: 500 });
}
const body = await request.text();
const signature = request.headers.get('webhook-signature') ?? '';
const expected = createHmac('sha1', webhookSecret).update(body).digest('hex');
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return new Response('Invalid signature', { status: 401 });
}
const response = await fetch(
`https://api-bridge.clever-cloud.com/v2/organisations/${orgaId}/applications/${appId}/instances`,
{
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
},
);
if (!response.ok) {
const error = await response.text();
return new Response(`Clever Cloud API error: ${response.status} ${error}`, { status: 502 });
}
return new Response('Rebuild triggered', { status: 200 });
};

View file

@ -14,6 +14,9 @@ import { companyInfo } from '../data';
<!-- Header --> <!-- Header -->
<div class="animate-entrance text-center mb-16 md:mb-24"> <div class="animate-entrance text-center mb-16 md:mb-24">
<h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8 leading-tight">La <span class="text-dream-coral italic">Compagnie</span></h1> <h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8 leading-tight">La <span class="text-dream-coral italic">Compagnie</span></h1>
<p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed">
{companyInfo.description}
</p>
</div> </div>
<!-- Présentation --> <!-- Présentation -->

View file

@ -54,33 +54,39 @@ body {
animation-delay: 4s; animation-delay: 4s;
} }
/* Entrance animations */ /* Scroll-triggered animations (Intersection Observer) */
.fade-up { .fade-up {
animation: fadeUp 0.6s ease both; opacity: 0;
transform: translateY(2rem);
transition: opacity 0.6s ease, transform 0.6s ease;
} }
@keyframes fadeUp { .fade-up.visible {
from { opacity: 0; transform: translateY(2rem); } opacity: 1;
to { opacity: 1; transform: translateY(0); } transform: translateY(0);
} }
.fade-right { .fade-right {
animation: fadeRight 0.6s ease both; opacity: 0;
transform: translateX(2rem);
transition: opacity 0.6s ease, transform 0.6s ease;
} }
@keyframes fadeRight { .fade-right.visible {
from { opacity: 0; transform: translateX(2rem); } opacity: 1;
to { opacity: 1; transform: translateX(0); } transform: translateX(0);
} }
.fade-scale { .fade-scale {
animation: fadeScale 1s ease both; opacity: 0;
transform: scale(0.9);
transition: opacity 1s ease, transform 1s ease;
} }
@keyframes fadeScale { .fade-scale.visible {
from { opacity: 0; transform: scale(0.9); } opacity: 1;
to { opacity: 1; transform: scale(1); } transform: scale(1);
} }
/* Page entrance animation */ /* Page entrance animation */