fullstuckdev commited on
Commit
fff42e3
·
verified ·
1 Parent(s): 290468f

Upload 26 files

Browse files
README.md CHANGED
@@ -1,12 +1,36 @@
1
- ---
2
- title: Warisan Nusantara
3
- emoji: 🏆
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- app_port: 3000
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ 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.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
next.config.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+ /** @type {import('next').NextConfig} */
3
+ const nextConfig: NextConfig = {
4
+ images: {
5
+ domains: ['*'],
6
+ dangerouslyAllowSVG: true,
7
+ remotePatterns: [
8
+ {
9
+ protocol: 'https',
10
+ hostname: '*',
11
+ },
12
+ ],
13
+ },
14
+ output: 'standalone',
15
+
16
+ experimental: {
17
+ serverComponentsExternalPackages: ['sharp', 'onnxruntime-node'],
18
+ },
19
+ };
20
+
21
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "budaya-indonesia",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@huggingface/inference": "^2.8.1",
13
+ "framer-motion": "^11.11.17",
14
+ "next": "15.0.3",
15
+ "openai": "^4.73.0",
16
+ "react": "^18.2.0",
17
+ "react-dom": "^18.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^20",
21
+ "@types/react": "^18",
22
+ "@types/react-dom": "^18",
23
+ "eslint": "^8",
24
+ "eslint-config-next": "15.0.3",
25
+ "postcss": "^8",
26
+ "tailwindcss": "^3.4.1",
27
+ "typescript": "^5"
28
+ }
29
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/app/api/generate-description/route.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { OpenAI } from "openai";
3
+ import { cultures } from '@/data/cultures';
4
+
5
+ const client = new OpenAI({
6
+ baseURL: "https://api-inference.huggingface.co/v1/",
7
+ apiKey: process.env.NEXT_PUBLIC_HUGGINGFACE_API_KEY,
8
+ });
9
+
10
+ export async function POST(request: Request) {
11
+ type RequestBody = {
12
+ cultureName: typeof cultures[number];
13
+ };
14
+
15
+ const body = await request.json() as RequestBody;
16
+ const { cultureName } = body;
17
+
18
+ try {
19
+ const chatCompletion = await client.chat.completions.create({
20
+ model: "meta-llama/Llama-3.1-70B-Instruct",
21
+ messages: [
22
+ {
23
+ role: "system",
24
+ content: "You are an expert in Indonesian culture. Provide detailed, accurate information in Indonesian language."
25
+ },
26
+ {
27
+ role: "user",
28
+ content: `Jelaskan tentang budaya Indonesia "${cultureName}" dalam 2-3 paragraf yang informatif. Berikan informasi tentang sejarah, makna, dan karakteristik uniknya.`
29
+ }
30
+ ],
31
+ max_tokens: 500,
32
+ temperature: 0.7,
33
+ });
34
+
35
+ const description = chatCompletion.choices[0]?.message?.content || "";
36
+ return NextResponse.json({ description });
37
+ } catch (error) {
38
+ console.error("Error details:", error);
39
+ return NextResponse.json({ error: "Failed to generate description" }, { status: 500 });
40
+ }
41
+ }
src/app/api/generate-image/route.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function POST(request: Request) {
4
+ const { prompt } = await request.json();
5
+
6
+ try {
7
+ const response = await fetch(
8
+ "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-3.5-large",
9
+ {
10
+ headers: {
11
+ Authorization: `Bearer ${process.env.NEXT_PUBLIC_HUGGINGFACE_API_KEY}`,
12
+ "Content-Type": "application/json",
13
+ },
14
+ method: "POST",
15
+ body: JSON.stringify({
16
+ inputs: prompt,
17
+ parameters: {
18
+ negative_prompt: "blurry, bad quality, distorted",
19
+ num_inference_steps: 30,
20
+ guidance_scale: 7.5,
21
+ }
22
+ }),
23
+ }
24
+ );
25
+
26
+ if (!response.ok) {
27
+ throw new Error(`HTTP error! status: ${response.status}`);
28
+ }
29
+
30
+ const imageBlob = await response.blob();
31
+ const arrayBuffer = await imageBlob.arrayBuffer();
32
+ const buffer = Buffer.from(arrayBuffer);
33
+ const base64Image = buffer.toString('base64');
34
+
35
+ const contentType = response.headers.get('content-type') || 'image/jpeg';
36
+
37
+ return NextResponse.json({
38
+ imageUrl: `data:${contentType};base64,${base64Image}`,
39
+ success: true
40
+ });
41
+ } catch (error) {
42
+ console.error('Image generation error:', error);
43
+ return NextResponse.json({
44
+ error: 'Failed to generate image',
45
+ success: false,
46
+ }, { status: 500 });
47
+ }
48
+ }
src/app/favicon.ico ADDED
src/app/fonts/GeistMonoVF.woff ADDED
Binary file (67.9 kB). View file
 
src/app/fonts/GeistVF.woff ADDED
Binary file (66.3 kB). View file
 
src/app/globals.css ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: #ffffff;
7
+ --foreground: #171717;
8
+ }
9
+
10
+ @media (prefers-color-scheme: dark) {
11
+ :root {
12
+ --background: #0a0a0a;
13
+ --foreground: #ededed;
14
+ }
15
+ }
16
+
17
+ body {
18
+ color: var(--foreground);
19
+ background: var(--background);
20
+ font-family: Arial, Helvetica, sans-serif;
21
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Logo } from '@/components/Logo';
2
+ import type { Metadata } from "next";
3
+ import localFont from "next/font/local";
4
+ import "./globals.css";
5
+
6
+ const geistSans = localFont({
7
+ src: "./fonts/GeistVF.woff",
8
+ variable: "--font-geist-sans",
9
+ weight: "100 900",
10
+ });
11
+ const geistMono = localFont({
12
+ src: "./fonts/GeistMonoVF.woff",
13
+ variable: "--font-geist-mono",
14
+ weight: "100 900",
15
+ });
16
+
17
+ export const metadata: Metadata = {
18
+ title: "Warisan Nusantara | Ragam Warisan Budaya Indonesia",
19
+ description: "Jelajahi keberagaman warisan budaya Indonesia, dari seni tradisional, kuliner khas, hingga adat istiadat yang memperkaya Nusantara.",
20
+ keywords: "budaya indonesia, warisan budaya, seni tradisional, kuliner indonesia, adat istiadat",
21
+ icons: {
22
+ icon: [
23
+ {
24
+ url: 'data:image/svg+xml;base64,' + btoa(`
25
+ <svg width="32" height="32" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
26
+ <rect width="512" height="512" fill="#DC2626"/>
27
+ <path d="M256 48 C384 48, 464 128, 464 256 C464 384, 384 464, 256 464 C128 464, 48 384, 48 256 C48 128, 128 48, 256 48Z" stroke="white" stroke-width="24" fill="none"/>
28
+ <circle cx="256" cy="176" r="48" fill="white" opacity="0.95"/>
29
+ <circle cx="336" cy="256" r="48" fill="white" opacity="0.95"/>
30
+ <circle cx="256" cy="336" r="48" fill="white" opacity="0.95"/>
31
+ <circle cx="176" cy="256" r="48" fill="white" opacity="0.95"/>
32
+ <circle cx="256" cy="256" r="32" fill="white"/>
33
+ <path d="M256 88 C344 88, 424 168, 424 256 C424 344, 344 424, 256 424 C168 424, 88 344, 88 256 C88 168, 168 88, 256 88Z" stroke="white" stroke-width="8" fill="none" opacity="0.6"/>
34
+ </svg>
35
+ `),
36
+ sizes: 'any',
37
+ type: 'image/svg+xml'
38
+ }
39
+ ]
40
+ },
41
+ openGraph: {
42
+ title: "Warisan Nusantara",
43
+ description: "Menjelajahi Ragam Warisan Budaya Indonesia",
44
+ locale: "id_ID",
45
+ type: "website",
46
+ images: [
47
+ {
48
+ url: '/og-image.png',
49
+ width: 1200,
50
+ height: 630,
51
+ alt: 'Warisan Nusantara'
52
+ }
53
+ ]
54
+ },
55
+ };
56
+
57
+ export default function RootLayout({
58
+ children,
59
+ }: Readonly<{
60
+ children: React.ReactNode;
61
+ }>) {
62
+ return (
63
+ <html lang="id">
64
+ <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
65
+ <nav className="fixed top-0 left-0 right-0 bg-white/80 backdrop-blur-sm z-50 border-b border-red-100">
66
+ <div className="max-w-4xl mx-auto px-4 py-3 flex items-center gap-3">
67
+ <Logo className="text-red-600" />
68
+ <span className="font-bold text-red-800 text-lg">Warisan Nusantara</span>
69
+ </div>
70
+ </nav>
71
+ <div className="pt-16">
72
+ {children}
73
+ </div>
74
+ </body>
75
+ </html>
76
+ );
77
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState } from 'react';
4
+ import { cultures } from '@/data/cultures';
5
+ import { generateDescription, generateImage } from '@/services/ai';
6
+ import { motion, AnimatePresence } from 'framer-motion';
7
+
8
+ export default function Home() {
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+ const [showPreview, setShowPreview] = useState(false);
12
+ const [culture, setCulture] = useState<{
13
+ name: string;
14
+ description: string;
15
+ imageUrl: string;
16
+ } | null>(null);
17
+
18
+ const generateRandomCulture = async () => {
19
+ setLoading(true);
20
+ setError(null);
21
+ try {
22
+ const randomCulture = cultures[Math.floor(Math.random() * cultures.length)];
23
+
24
+ const [description, imageData] = await Promise.all([
25
+ generateDescription(randomCulture),
26
+ generateImage(`Indonesian traditional culture ${randomCulture}, professional photography style, high quality, detailed, 4k resolution`)
27
+ ]);
28
+
29
+ if (!imageData || !imageData.imageUrl) {
30
+ throw new Error('Failed to generate image');
31
+ }
32
+
33
+ setCulture({
34
+ name: randomCulture,
35
+ description: description || '',
36
+ imageUrl: imageData.imageUrl
37
+ });
38
+ } catch (error) {
39
+ console.error('Error:', error);
40
+ setError('Failed to generate content. Please try again.');
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <main className="min-h-screen p-8 bg-gradient-to-br from-orange-100 to-red-100">
48
+ <div className="max-w-4xl mx-auto">
49
+ <div className="text-center mb-12">
50
+ <motion.h1
51
+ initial={{ opacity: 0, y: -20 }}
52
+ animate={{ opacity: 1, y: 0 }}
53
+ className="text-5xl font-bold mb-3 text-red-800"
54
+ >
55
+ Warisan Nusantara
56
+ </motion.h1>
57
+ <motion.p
58
+ initial={{ opacity: 0, y: -20 }}
59
+ animate={{ opacity: 1, y: 0 }}
60
+ transition={{ delay: 0.2 }}
61
+ className="text-xl text-red-700/80"
62
+ >
63
+ Menjelajahi Ragam Warisan Budaya Indonesia
64
+ </motion.p>
65
+ </div>
66
+
67
+ <motion.button
68
+ whileHover={{ scale: 1.02 }}
69
+ whileTap={{ scale: 0.98 }}
70
+ onClick={generateRandomCulture}
71
+ disabled={loading}
72
+ className="w-full max-w-md mx-auto block px-6 py-3 bg-red-600 text-white rounded-lg shadow-lg hover:bg-red-700 transition-colors disabled:bg-gray-400"
73
+ >
74
+ {loading ? (
75
+ <div className="flex items-center justify-center">
76
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white mr-2"></div>
77
+ Memuat...
78
+ </div>
79
+ ) : (
80
+ 'Jelajahi Budaya'
81
+ )}
82
+ </motion.button>
83
+
84
+ {error && (
85
+ <motion.div
86
+ initial={{ opacity: 0 }}
87
+ animate={{ opacity: 1 }}
88
+ className="mt-4 p-4 bg-red-100 text-red-700 rounded-lg"
89
+ >
90
+ {error}
91
+ </motion.div>
92
+ )}
93
+
94
+ <AnimatePresence>
95
+ {culture && (
96
+ <motion.div
97
+ initial={{ opacity: 0, y: 20 }}
98
+ animate={{ opacity: 1, y: 0 }}
99
+ exit={{ opacity: 0, y: -20 }}
100
+ className="mt-12 bg-white rounded-xl shadow-xl overflow-hidden"
101
+ >
102
+ <div className="relative h-[400px] w-full cursor-pointer" onClick={() => setShowPreview(true)}>
103
+ {culture.imageUrl && (
104
+ <img
105
+ src={culture.imageUrl}
106
+ alt={culture.name}
107
+ className="w-full h-full object-contain"
108
+ onError={(e) => {
109
+ console.error('Image failed to load');
110
+ e.currentTarget.src = '/placeholder-image.jpg';
111
+ }}
112
+ />
113
+ )}
114
+ <div className="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-10 transition-all duration-300 flex items-center justify-center">
115
+ <span className="text-white opacity-0 hover:opacity-100 transition-opacity duration-300">
116
+ Klik untuk memperbesar
117
+ </span>
118
+ </div>
119
+ </div>
120
+ <div className="p-6">
121
+ <h2 className="text-2xl font-bold mb-4 text-red-800">
122
+ {culture.name}
123
+ </h2>
124
+ <p className="text-gray-700 leading-relaxed whitespace-pre-wrap">
125
+ {culture.description}
126
+ </p>
127
+ </div>
128
+ </motion.div>
129
+ )}
130
+ </AnimatePresence>
131
+
132
+ <AnimatePresence>
133
+ {showPreview && culture && (
134
+ <motion.div
135
+ initial={{ opacity: 0 }}
136
+ animate={{ opacity: 1 }}
137
+ exit={{ opacity: 0 }}
138
+ className="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"
139
+ onClick={() => setShowPreview(false)}
140
+ >
141
+ <motion.div
142
+ initial={{ scale: 0.9 }}
143
+ animate={{ scale: 1 }}
144
+ exit={{ scale: 0.9 }}
145
+ className="relative max-w-[90vw] max-h-[90vh]"
146
+ >
147
+ <img
148
+ src={culture.imageUrl}
149
+ alt={culture.name}
150
+ className="max-w-full max-h-[90vh] object-contain"
151
+ />
152
+ <button
153
+ className="absolute top-4 right-4 text-white bg-black bg-opacity-50 rounded-full p-2 hover:bg-opacity-70"
154
+ onClick={() => setShowPreview(false)}
155
+ >
156
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
157
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
158
+ </svg>
159
+ </button>
160
+ </motion.div>
161
+ </motion.div>
162
+ )}
163
+ </AnimatePresence>
164
+ </div>
165
+ </main>
166
+ );
167
+ }
src/components/Logo.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface LogoProps {
4
+ className?: string;
5
+ size?: number;
6
+ }
7
+
8
+ export const Logo: React.FC<LogoProps> = ({ className = "", size = 40 }) => {
9
+ return (
10
+ <svg
11
+ width={size}
12
+ height={size}
13
+ viewBox="0 0 512 512"
14
+ fill="none"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ className={className}
17
+ >
18
+ {/* Background */}
19
+ <rect width="512" height="512" fill="currentColor"/>
20
+
21
+ {/* Ornamental Border - Inspired by Indonesian Batik */}
22
+ <path
23
+ d="M256 48
24
+ C384 48, 464 128, 464 256
25
+ C464 384, 384 464, 256 464
26
+ C128 464, 48 384, 48 256
27
+ C48 128, 128 48, 256 48Z"
28
+ stroke="white"
29
+ strokeWidth="24"
30
+ fill="none"
31
+ />
32
+
33
+ {/* Central Pattern - Inspired by Kawung */}
34
+ <circle cx="256" cy="176" r="48" fill="white" opacity="0.95"/>
35
+ <circle cx="336" cy="256" r="48" fill="white" opacity="0.95"/>
36
+ <circle cx="256" cy="336" r="48" fill="white" opacity="0.95"/>
37
+ <circle cx="176" cy="256" r="48" fill="white" opacity="0.95"/>
38
+
39
+ {/* Center Circle */}
40
+ <circle cx="256" cy="256" r="32" fill="white"/>
41
+
42
+ {/* Decorative Lines */}
43
+ <path
44
+ d="M256 88
45
+ C344 88, 424 168, 424 256
46
+ C424 344, 344 424, 256 424
47
+ C168 424, 88 344, 88 256
48
+ C88 168, 168 88, 256 88Z"
49
+ stroke="white"
50
+ strokeWidth="8"
51
+ fill="none"
52
+ opacity="0.6"
53
+ />
54
+ </svg>
55
+ );
56
+ };
src/data/cultures.ts ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const cultures = [
2
+ // Aceh
3
+ "Tari Saman dari Aceh",
4
+ "Tari Seudati dari Aceh",
5
+ "Rapa'i Geleng dari Aceh",
6
+ "Didong dari Gayo",
7
+
8
+ // Sumatera Utara
9
+ "Tari Tor-Tor dari Batak",
10
+ "Gordang Sambilan dari Mandailing",
11
+ "Ulos dari Batak",
12
+ "Tari Serampang Dua Belas",
13
+
14
+ // Sumatera Barat
15
+ "Tari Piring dari Minangkabau",
16
+ "Randai dari Minangkabau",
17
+ "Songket Minangkabau",
18
+ "Silat Minangkabau",
19
+
20
+ // Riau
21
+ "Tari Zapin Melayu",
22
+ "Gurindam Dua Belas",
23
+ "Tenun Siak",
24
+
25
+ // Jambi
26
+ "Tari Sekapur Sirih",
27
+ "Tenun Batanghari",
28
+ "Festival Danau Kerinci",
29
+
30
+ // Sumatera Selatan
31
+ "Tari Tanggai",
32
+ "Songket Palembang",
33
+ "Rumah Limas",
34
+
35
+ // Bengkulu
36
+ "Tari Andun",
37
+ "Kerajinan Kulit Kayu",
38
+
39
+ // Lampung
40
+ "Tari Melinting",
41
+ "Kain Tapis",
42
+ "Sekura dari Lampung Barat",
43
+
44
+ // Jawa Barat
45
+ "Angklung dari Jawa Barat",
46
+ "Wayang Golek Sunda",
47
+ "Tari Jaipong",
48
+ "Pencak Silat Sunda",
49
+ "Kecapi Suling",
50
+ "Tari Merak",
51
+
52
+ // Jakarta
53
+ "Tari Topeng Betawi",
54
+ "Ondel-ondel",
55
+ "Lenong Betawi",
56
+ "Kerak Telor",
57
+
58
+ // Jawa Tengah
59
+ "Wayang Kulit dari Jawa",
60
+ "Tari Serimpi",
61
+ "Tari Gambyong",
62
+ "Karawitan Jawa",
63
+ "Batik Solo",
64
+ "Batik Pekalongan",
65
+
66
+ // Yogyakarta
67
+ "Tari Bedhaya",
68
+ "Wayang Wong",
69
+ "Batik Yogyakarta",
70
+ "Sekaten",
71
+
72
+ // Jawa Timur
73
+ "Reog Ponorogo",
74
+ "Tari Remo",
75
+ "Ludruk",
76
+ "Karapan Sapi dari Madura",
77
+ "Batik Madura",
78
+
79
+ // Bali
80
+ "Tari Pendet dari Bali",
81
+ "Tari Kecak dari Bali",
82
+ "Tari Legong",
83
+ "Barong dan Rangda",
84
+ "Wayang Kulit Bali",
85
+ "Gamelan Bali",
86
+
87
+ // Nusa Tenggara Barat
88
+ "Tari Gandrung",
89
+ "Tenun Songket Lombok",
90
+ "Peresean",
91
+
92
+ // Nusa Tenggara Timur
93
+ "Tari Caci dari Manggarai",
94
+ "Tenun Ikat Flores",
95
+ "Sasando dari Rote",
96
+
97
+ // Kalimantan Barat
98
+ "Tari Monong",
99
+ "Dayak Gawai",
100
+
101
+ // Kalimantan Tengah
102
+ "Tari Mandau",
103
+ "Upacara Tiwah",
104
+
105
+ // Kalimantan Selatan
106
+ "Tari Baksa Kembang",
107
+ "Sasirangan",
108
+
109
+ // Kalimantan Timur
110
+ "Tari Gantar",
111
+ "Upacara Kwangkay",
112
+
113
+ // Kalimantan Utara
114
+ "Tari Kancet Ledo",
115
+ "Blontang",
116
+
117
+ // Sulawesi Utara
118
+ "Tari Maengket",
119
+ "Tari Kabasaran",
120
+ "Kolintang",
121
+
122
+ // Sulawesi Tengah
123
+ "Tari Moduai",
124
+ "Balia",
125
+
126
+ // Sulawesi Selatan
127
+ "Tari Pakarena",
128
+ "Tenun Sutera Bugis",
129
+ "Ma'badong",
130
+
131
+ // Sulawesi Tenggara
132
+ "Tari Lariangi",
133
+ "Tenun Bomba",
134
+
135
+ // Gorontalo
136
+ "Tari Saronde",
137
+ "Dana-dana",
138
+
139
+ // Maluku
140
+ "Tari Cakalele",
141
+ "Tari Lenso",
142
+ "Pela Gandong",
143
+
144
+ // Maluku Utara
145
+ "Tari Legu Salai",
146
+ "Tari Soya-soya",
147
+
148
+ // Papua Barat
149
+ "Tari Yospan",
150
+ "Ukiran Asmat",
151
+
152
+ // Papua
153
+ "Tari Selamat Datang",
154
+ "Koteka",
155
+ "Honai",
156
+
157
+ // Kerajinan Tradisional
158
+ "Batik",
159
+ "Tenun Ikat",
160
+ "Songket",
161
+ "Anyaman Rotan",
162
+ "Ukiran Kayu",
163
+
164
+ // Seni Musik
165
+ "Gamelan Jawa",
166
+ "Talempong",
167
+ "Tifa",
168
+ "Rebana",
169
+
170
+ // Seni Pertunjukan
171
+ "Ketoprak",
172
+ "Randai",
173
+ "Mamanda",
174
+ "Debus",
175
+
176
+ // Upacara Adat
177
+ "Ngaben di Bali",
178
+ "Kasada di Tengger",
179
+ "Rambu Solo di Toraja",
180
+ "Seren Taun di Sunda",
181
+
182
+ "Tari Saman dari Aceh",
183
+ "Tari Seudati dari Aceh",
184
+ "Tari Ratoh Jaroe",
185
+ "Tari Ranup Lampuan",
186
+ "Tari Likok Pulo",
187
+ "Tari Tarek Pukat",
188
+ "Tari Rapa'i Geleng",
189
+ "Tari Dampeng",
190
+ "Tari Laweut",
191
+ "Tari Guel",
192
+
193
+ // Musik Tradisional Aceh
194
+ "Rapa'i dari Aceh",
195
+ "Serune Kalee",
196
+ "Geundrang",
197
+ "Canang",
198
+ "Rebana Aceh",
199
+
200
+ // Pakaian Tradisional Aceh
201
+ "Pakaian Ulee Balang",
202
+ "Pakaian Daro Baro",
203
+ "Pakaian Linto Baro",
204
+
205
+ // Rumah Adat Aceh
206
+ "Rumoh Aceh",
207
+ "Krong Bade",
208
+
209
+ // Senjata Tradisional Aceh
210
+ "Rencong",
211
+ "Peudeung",
212
+ "Siwah",
213
+
214
+ // Makanan Tradisional Aceh
215
+ "Mie Aceh",
216
+ "Kuah Pliek U",
217
+ "Ayam Tangkap",
218
+ "Kuah Beulangong",
219
+ "Sie Reuboh",
220
+ "Martabak Aceh",
221
+ "Timphan",
222
+ "Kue Adee",
223
+
224
+ // Kerajinan Tradisional Aceh
225
+ "Kerawang Gayo",
226
+ "Bordiran Aceh",
227
+ "Anyaman Pandan",
228
+ "Kerajinan Kuningan",
229
+
230
+ // Upacara Adat Aceh
231
+ "Peusijuek",
232
+ "Tueng Dara Baro",
233
+ "Peutron Aneuk",
234
+
235
+ // SUMATERA UTARA
236
+ // Tarian Tradisional Batak
237
+ "Tari Tor-Tor",
238
+ "Tari Sigale-gale",
239
+ "Tari Mogang",
240
+ "Tari Tembut-tembut",
241
+ "Tari Sipitu Cawan",
242
+ "Tari Serampang Dua Belas",
243
+ "Tari Gubang",
244
+ "Tari Endeng-endeng",
245
+
246
+ // Musik Tradisional Batak
247
+ "Gondang Sabangunan",
248
+ "Gordang Sambilan",
249
+ "Hasapi",
250
+ "Garantung",
251
+ "Sarune",
252
+ "Taganing",
253
+
254
+ // Pakaian Tradisional Batak
255
+ "Ulos Batak",
256
+ "Ulos Ragi Hotang",
257
+ "Ulos Ragi Idup",
258
+ "Ulos Sadum",
259
+ "Ulos Mangiring",
260
+ "Ulos Bintang Maratur",
261
+
262
+ // Rumah Adat Batak
263
+ "Rumah Bolon",
264
+ "Rumah Gorga",
265
+ "Sopo Bolon",
266
+
267
+ // Senjata Tradisional Batak
268
+ "Piso Gaja Dompak",
269
+ "Piso Halasan",
270
+ "Podang",
271
+
272
+ // Makanan Tradisional Batak
273
+ "Arsik",
274
+ "Saksang",
275
+ "Sangsang",
276
+ "Na Niura",
277
+ "Dengke Mas Na Niura",
278
+ "Sambal Andaliman",
279
+ "Na Tinombur",
280
+
281
+ // Kerajinan Tradisional Batak
282
+ "Tenun Ulos",
283
+ "Gorga",
284
+ "Ukiran Batak",
285
+ "Anyaman Pandan",
286
+
287
+ // Upacara Adat Batak
288
+ "Mangulosi",
289
+ "Martutuaek",
290
+ "Mangongkal Holi",
291
+ "Manulangi",
292
+ "Mamongoti",
293
+
294
+ // Tradisi Batak
295
+ "Dalihan Na Tolu",
296
+ "Marga System",
297
+ "Partuturan",
298
+
299
+ // Alat Musik Tradisional Batak
300
+ "Gondang",
301
+ "Hasapi",
302
+ "Sulim",
303
+ "Sarune",
304
+ "Ogung",
305
+
306
+ // MINANGKABAU (SUMATERA BARAT)
307
+ // Tarian Tradisional
308
+ "Tari Piring",
309
+ "Tari Payung",
310
+ "Tari Pasambahan",
311
+ "Tari Indang",
312
+ "Tari Galombang",
313
+ "Tari Rantak",
314
+
315
+ // Musik Tradisional
316
+ "Talempong",
317
+ "Saluang",
318
+ "Rabab",
319
+ "Gandang Tasa",
320
+
321
+ // Pakaian Tradisional
322
+ "Baju Kurung",
323
+ "Baju Batabue",
324
+ "Tengkuluk",
325
+ "Suntiang",
326
+
327
+ // Rumah Adat
328
+ "Rumah Gadang",
329
+ "Rangkiang",
330
+
331
+ // Seni Pertunjukan
332
+ "Randai",
333
+ "Silek",
334
+ "Saluang Dendang",
335
+ "Bagurau",
336
+
337
+ // Makanan Tradisional
338
+ "Rendang",
339
+ "Sate Padang",
340
+ "Dendeng Balado",
341
+ "Gulai Kapau",
342
+ "Lamang",
343
+ "Sala Lauak",
344
+
345
+ // Kerajinan Tradisional
346
+ "Songket Minang",
347
+ "Sulaman Benang Emas",
348
+ "Ukiran Minang",
349
+
350
+ // Upacara Adat
351
+ "Batagak Penghulu",
352
+ "Baralek",
353
+ "Turun Mandi",
354
+ "Sunat Rasul",
355
+
356
+ // Tradisi
357
+ "Matrilineal",
358
+ "Mamak-Kamanakan",
359
+ "Marantau",
360
+ ];
src/services/ai.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function generateDescription(cultureName: string) {
2
+ const response = await fetch('/api/generate-description', {
3
+ method: 'POST',
4
+ headers: {
5
+ 'Content-Type': 'application/json',
6
+ },
7
+ body: JSON.stringify({ cultureName }),
8
+ });
9
+
10
+ const data = await response.json();
11
+ return data.description;
12
+ }
13
+
14
+ export async function generateImage(prompt: string): Promise<{ imageUrl: string }> {
15
+ const response = await fetch('/api/generate-image', {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ body: JSON.stringify({ prompt }),
21
+ });
22
+
23
+ const data = await response.json();
24
+
25
+ if (!response.ok) {
26
+ throw new Error(data.error || 'Failed to generate image');
27
+ }
28
+
29
+ return data;
30
+ }
src/types/culture.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface Culture {
2
+ name: string;
3
+ description: string;
4
+ imageUrl: string;
5
+ }
tailwind.config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ export default {
4
+ content: [
5
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ colors: {
12
+ background: "var(--background)",
13
+ foreground: "var(--foreground)",
14
+ },
15
+ },
16
+ },
17
+ plugins: [],
18
+ } satisfies Config;
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }
vercel.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "buildCommand": "next build",
3
+ "devCommand": "next dev",
4
+ "installCommand": "npm install",
5
+ "framework": "nextjs",
6
+ "outputDirectory": ".next"
7
+ }