diff --git a/stadstuin/.gitignore b/stadstuin/.gitignore new file mode 100644 index 0000000..9dbf82b --- /dev/null +++ b/stadstuin/.gitignore @@ -0,0 +1,142 @@ +# Node.js +node_modules/ +.pnp +.pnp.js + +# Next.js +.next/ +out/ +build/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environment variables +.env +.env.local +.env.*.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Production +build +.next +.nuxt +.nitro +.cache +.temp +.tmp + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Editor directories and files +.vercel +.next +.vercel_build_output + +# Local development +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local development files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.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 diff --git a/stadstuin/README.md b/stadstuin/README.md new file mode 100644 index 0000000..7469875 --- /dev/null +++ b/stadstuin/README.md @@ -0,0 +1,157 @@ +# Stadstuin Project + +A full-stack application with a Next.js frontend and Python Flask backend, built with TypeScript and modern web technologies. + +## Project Structure + +``` +. +├── backend/ # Python Flask backend +│ ├── app.py # Main application entry point +│ └── requirements.txt # Python dependencies +├── frontend/ # Next.js frontend +│ ├── public/ # Static files +│ ├── src/ # Source code +│ │ ├── app/ # App router pages +│ │ ├── components/ # React components +│ │ └── utils/ # Utility functions +│ ├── package.json # Frontend dependencies +│ └── tailwind.config.js # Tailwind CSS configuration +├── .gitignore +└── package.json # Monorepo configuration +``` + +## Prerequisites + +- Node.js 18+ and npm/yarn +- Python 3.8+ +- pip +- virtualenv (recommended for Python development) + +## Getting Started + +### Monorepo Setup + +1. Install Node.js dependencies for the root project: + ```bash + npm install + ``` + +### Backend Development + +1. Navigate to the backend directory and set up a virtual environment: + ```bash + cd backend + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +2. Install Python dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Create a `.env` file with your environment variables: + ``` + OPENAI_API_KEY= + OPENAI_API_URL= + DALLE_URL= + DALLE_KEY= + ``` + +4. Start the backend server: + ```bash + python app.py + ``` + The backend will be available at `http://localhost:5000` + +### Frontend Development + +1. From the project root, start the Next.js development server: + ```bash + npm run dev:frontend + ``` + The frontend will be available at `http://localhost:3000` + +## Available Scripts + +- `npm run dev:frontend` - Start the Next.js development server +- `npm run dev:backend` - Start the Python backend server +- `npm run build` - Build the Next.js application for production +- `npm run start` - Start the production server +- `npm run lint` - Run ESLint + +## Tech Stack + +- **Frontend**: + - Next.js 14 + - React 18 + - TypeScript + - Tailwind CSS + - Axios for API requests + +- **Backend**: + - Python 3.8+ + - Flask + - RESTful API + +### Frontend Setup + +1. Navigate to the frontend directory: +```bash +cd frontend +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Create a `.env.local` file in the frontend directory with: +``` +NEXT_PUBLIC_API_BASE_URL=http://localhost:5000 +``` + +4. Start the frontend development server: +```bash +npm run dev +``` +The frontend will run on `http://localhost:3000` + +## Running the Application + +1. Start the backend server first: +```bash +cd backend +python app.py +``` + +2. In a new terminal, start the frontend: +```bash +cd frontend +npm run dev +``` + +The application will be available at `http://localhost:3000`. The frontend communicates with the backend through the API running on `http://localhost:5000`. + +## Features + +- Users can submit wishes for their ideal city garden +- Wishes are combined into a prompt for image generation +- Uses Azure OpenAI's DALL-E 3 model to generate images +- Responsive design that works on both desktop and mobile +- Real-time validation and error handling +- Toast notifications for user feedback + +## Development + +### Backend API Endpoints + +- `POST /build_prompt`: Combines wishes into a prompt for image generation +- `POST /generate_image`: Generates an image using Azure OpenAI's DALL-E 3 model + +### Frontend Components + +- `WishInput`: Form for submitting wishes +- `WishBubbleList`: Displays submitted wishes +- Main page layout with image generation functionality diff --git a/stadstuin/backend/.env.example b/stadstuin/backend/.env.example new file mode 100644 index 0000000..cee5c29 --- /dev/null +++ b/stadstuin/backend/.env.example @@ -0,0 +1,5 @@ +OPENAI_API_KEY= +OPENAI_API_URL= +DALLE_URL= +DALLE_KEY= + diff --git a/stadstuin/backend/app.py b/stadstuin/backend/app.py new file mode 100644 index 0000000..086d50f --- /dev/null +++ b/stadstuin/backend/app.py @@ -0,0 +1,139 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from dotenv import load_dotenv +import os +import requests + +load_dotenv() + +# Configure OpenAI +openai_api_key = os.getenv('OPENAI_API_KEY') +openai_api_url = os.getenv('OPENAI_API_URL') + +dalle_key = os.getenv('DALLE_KEY') +dalle_url = os.getenv('DALLE_URL') +print(dalle_key, dalle_url) +app = Flask(__name__) +CORS(app, origins=["http://localhost:3000", + "http://127.0.0.1:3000"]) + + +@app.route('/') +def hello(): + return 'Stadstuin Backend API' + + +@app.route('/build_prompt', methods=['POST']) +def build_prompt(): + try: + data = request.get_json() + wishes = data.get('wishes', []) + + # Create the initial prompt with the wishes + initial_prompt = """Below are individual wishes for our shared Amsterdam stadstuin. + Combine every wish into **one** richly detailed, realistic scene descriptionthat DALL-E 3 can render. + Follow the CO-STAR guard-rails you have been given. + TARGET LENGTH ≤ 1000 characters.""" + + for wish in wishes: + initial_prompt += f"– {wish['name']}: {wish['wish']}\n" + + headers = { + "Content-Type": "application/json", + "api-key": openai_api_key + } + + payload = { + "messages": [ + {"role": "system", "content": """Context (C) + You are an expert prompt composer for DALL-E 3. Your task is to merge multiple citizen wishes into a single, clear yet realistic image description of a shared “stadstuin” (neighbourhood garden) in Amsterdam. + + Objective (O) + Produce one self-contained text prompt (maximum 1000 characters) that DALL-E 3 can render directly, without any additional explanation. + + Style & Specificity (S) + • Be concise and precise: translate the residents’ wishes into a coherent visual description without unnecessary flourish. + • Include only plant species, materials, and features appropriate for Amsterdam’s temperate maritime climate (e.g., Dutch elm, European beech, tulips, brick pathways, reclaimed-timber benches). + • Exclude brand names, copyrighted characters, and photorealistic depictions of private individuals. + + Tone / Task (T) + Use descriptive language that reflects the residents’ own phrasing and spirit—no embellishments beyond their expressed wishes. + + Audience (A) + Primary: DALL-E 3 (the model). + Secondary: Amsterdam residents who will view the final artwork. + + Requirements / Restrictions (R) + • Ensure plausibility: no flying cars, fantasy creatures, skyscrapers, or tropical palms. + • Keep the setting clearly Amsterdam: reference canal-house façades, cycling paths, Dutch-design benches, etc. + • Create one cohesive scene—do not list wishes or use bullet points. + • Follow content policy: no hate speech, explicit nudity, violence, or personal data. + • Write in English, preserving Dutch place and plant names when authentic (e.g., “gracht”). + • Limit to 1000 characters—trim carefully if needed. + • ALWAYS ask DALL-E to generate a photorealistic image. + + Return only the completed prompt text—no commentary. + """}, + {"role": "user", "content": initial_prompt} + ], + "max_tokens": 1000, + "temperature": 0.7 + } + + # Call Azure OpenAI to generate a combined prompt + response = requests.post(openai_api_url, headers=headers, json=payload) + response_data = response.json() + + # Extract the generated prompt from the response + generated_prompt = response_data["choices"][0]["message"]["content"] + + return jsonify({ + 'prompt': generated_prompt + }) + except Exception as e: + print(f"Error: {str(e)}") + return jsonify({ + 'error': str(e) + }), 400 + + +@app.route('/generate_image', methods=['POST']) +def generate_image(): + try: + data = request.get_json() + prompt = data.get('prompt') + + if not prompt: + return jsonify({ + 'error': 'Prompt is required' + }), 400 + + # Generate image using Azure OpenAI + headers = { + 'Content-Type': 'application/json', + 'api-key': dalle_key + } + + payload = { + "model": "dall-e-3", + "prompt": prompt, + "size": "1024x1024", + "style": "vivid", + "quality": "standard", + "n": 1 + } + + response = requests.post(dalle_url, headers=headers, json=payload) + response.raise_for_status() + + return jsonify({ + 'imageUrl': response.json()['data'][0]['url'] + }) + except Exception as e: + return jsonify({ + 'error': str(e) + }), 500 + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/stadstuin/backend/package.json b/stadstuin/backend/package.json new file mode 100644 index 0000000..b12f1aa --- /dev/null +++ b/stadstuin/backend/package.json @@ -0,0 +1,8 @@ +{ + "name": "backend", + "version": "1.0.0", + "private": true, + "scripts": { + "run": "python -m venv venv && source venv/bin/activate && pip install -r requirements.txt && python app.py" + } +} diff --git a/stadstuin/frontend/.gitignore b/stadstuin/frontend/.gitignore new file mode 100644 index 0000000..a9b56bf --- /dev/null +++ b/stadstuin/frontend/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# Environment files +.env.local +.env.*.local + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/stadstuin/frontend/eslint.config.mjs b/stadstuin/frontend/eslint.config.mjs new file mode 100644 index 0000000..c85fb67 --- /dev/null +++ b/stadstuin/frontend/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/stadstuin/frontend/next.config.js b/stadstuin/frontend/next.config.js new file mode 100644 index 0000000..85ebad6 --- /dev/null +++ b/stadstuin/frontend/next.config.js @@ -0,0 +1,26 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'oaidalleapiprodscus.blob.core.windows.net', + }, + { + protocol: 'https', + hostname: 'dalleprodsec.blob.core.windows.net', + }, + ], + }, + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://localhost:5000/:path*', + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/stadstuin/frontend/next.config.ts b/stadstuin/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/stadstuin/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/stadstuin/frontend/package.json b/stadstuin/frontend/package.json new file mode 100644 index 0000000..cb9d1e1 --- /dev/null +++ b/stadstuin/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "axios": "^1.6.2", + "next": "^14.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1" + }, + "devDependencies": { + "@types/node": "^20.10.4", + "@types/react": "^18.2.42", + "@types/react-dom": "^18.2.17", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-config-next": "^14.0.4", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.3" + } +} diff --git a/stadstuin/frontend/postcss.config.js b/stadstuin/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/stadstuin/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/stadstuin/frontend/postcss.config.mjs b/stadstuin/frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/stadstuin/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/stadstuin/frontend/public/file.svg b/stadstuin/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/stadstuin/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stadstuin/frontend/public/globe.svg b/stadstuin/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/stadstuin/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stadstuin/frontend/public/next.svg b/stadstuin/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/stadstuin/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stadstuin/frontend/public/vercel.svg b/stadstuin/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/stadstuin/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stadstuin/frontend/public/window.svg b/stadstuin/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/stadstuin/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stadstuin/frontend/src/app/favicon.ico b/stadstuin/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/stadstuin/frontend/src/app/favicon.ico differ diff --git a/stadstuin/frontend/src/app/globals.css b/stadstuin/frontend/src/app/globals.css new file mode 100644 index 0000000..4f544f2 --- /dev/null +++ b/stadstuin/frontend/src/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-inter); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans), Arial, Helvetica, sans-serif; +} diff --git a/stadstuin/frontend/src/app/layout.tsx b/stadstuin/frontend/src/app/layout.tsx new file mode 100644 index 0000000..91331b5 --- /dev/null +++ b/stadstuin/frontend/src/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { Toaster } from "react-hot-toast"; + +const inter = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + + ); +} diff --git a/stadstuin/frontend/src/app/page.tsx b/stadstuin/frontend/src/app/page.tsx new file mode 100644 index 0000000..1b6414e --- /dev/null +++ b/stadstuin/frontend/src/app/page.tsx @@ -0,0 +1,164 @@ +'use client'; + +import Image from "next/image"; +import { useState } from 'react'; +import WishInput from '@/components/WishInput'; +import WishBubbleList from '@/components/WishBubbleList'; +import api from '@/utils/api'; +import { toast } from 'react-hot-toast'; + +export default function Home() { + const [wishes, setWishes] = useState<{ name: string; wish: string }[]>([]); + const [prompt, setPrompt] = useState(''); + const [imageUrl, setImageUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [isEditing, setIsEditing] = useState(false); + + const handleAddWish = (wish: { name: string; wish: string }) => { + setWishes((prev) => [...prev, wish]); + }; + + const handleCombineWishes = async () => { + if (wishes.length === 0) { + toast.error('Voeg eerst wensen toe voordat je wensen combineert'); + return; + } + + setErrorMessage(null); + setLoading(true); + + try { + // Get prompt from wishes + const promptResponse = await api.post('/build_prompt', { wishes }); + const generatedPrompt = (promptResponse.data as { prompt: string }).prompt; + setPrompt(generatedPrompt); + toast.success('Wensen succesvol gecombineerd!'); + } catch (err: unknown) { + const errorMessage = err instanceof Error + ? err.message + : typeof err === 'object' && err !== null && 'response' in err + ? (err as { response: { data: { error?: string } } }).response?.data?.error + : 'Er is een fout opgetreden bij het combineren van de wensen'; + + toast.error(errorMessage || 'Er is een fout opgetreden bij het combineren van de wensen'); + setErrorMessage(errorMessage || 'Er is een fout opgetreden bij het combineren van de wensen'); + } finally { + setLoading(false); + } + }; + + const toggleEditing = () => { + setIsEditing(!isEditing); + }; + + const handleBuildGarden = async () => { + if (!prompt) { + toast.error('Combineer eerst je wensen om een prompt te genereren'); + return; + } + + setErrorMessage(null); + setLoading(true); + + try { + // Generate image from prompt + const imageResponse = await api.post('/generate_image', { prompt }); + const imageUrl = (imageResponse.data as { imageUrl: string }).imageUrl; + + setImageUrl(imageUrl); + toast.success('Stadstuin succesvol gegenereerd!'); + } catch (err: unknown) { + const errorMessage = err instanceof Error + ? err.message + : typeof err === 'object' && err !== null && 'response' in err + ? (err as { response: { data: { error?: string } } }).response?.data?.error + : 'Er is een fout opgetreden bij het genereren van de stadstuin'; + + toast.error(errorMessage || 'Er is een fout opgetreden bij het genereren van de stadstuin'); + setErrorMessage(errorMessage || 'Er is een fout opgetreden bij het genereren van de stadstuin'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+

+ Bouw je eigen stadstuin +

+

+ Deel je wensen voor een groenere stad en laat ons je stadstuin ontwerpen! +

+
+ +
+ {/* Left: Input */} +
+ + + + {prompt && ( +
+ {isEditing + ?