aminskjen commited on
Commit
f2ef360
·
verified ·
1 Parent(s): 5ef55c7

Upload 11 files

Browse files
app.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash
4
+ from flask_sqlalchemy import SQLAlchemy
5
+ from flask_login import LoginManager, login_user, logout_user, login_required, current_user
6
+ import cohere
7
+ import requests
8
+ import uuid
9
+ from datetime import datetime
10
+ from sqlalchemy.orm import DeclarativeBase
11
+
12
+ # Set up logging for debugging
13
+ logging.basicConfig(level=logging.DEBUG)
14
+
15
+ class Base(DeclarativeBase):
16
+ pass
17
+
18
+ # Initialize Flask app
19
+ app = Flask(__name__)
20
+ app.secret_key = os.environ.get("SESSION_SECRET", "default_secret_key_for_dev")
21
+
22
+ # Database configuration
23
+ database_url = os.environ.get("DATABASE_URL")
24
+ if not database_url:
25
+ # Fallback for development
26
+ database_url = "sqlite:///inkboard.db"
27
+
28
+ app.config["SQLALCHEMY_DATABASE_URI"] = database_url
29
+ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
30
+ app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
31
+ 'pool_pre_ping': True,
32
+ "pool_recycle": 300,
33
+ }
34
+
35
+ # Initialize database
36
+ db = SQLAlchemy(app, model_class=Base)
37
+
38
+ # Initialize Flask-Login
39
+ login_manager = LoginManager()
40
+ login_manager.init_app(app)
41
+ login_manager.login_view = 'login'
42
+ login_manager.login_message = 'Please log in to access InkBoard'
43
+
44
+ # Initialize Cohere client for text generation
45
+ COHERE_API_KEY = os.environ.get("COHERE_API_KEY")
46
+ if not COHERE_API_KEY:
47
+ logging.error("COHERE_API_KEY environment variable not set")
48
+ cohere_client = None
49
+ else:
50
+ cohere_client = cohere.Client(COHERE_API_KEY)
51
+
52
+ # Initialize Hugging Face for image generation
53
+ HUGGINGFACE_API_KEY = os.environ.get("HUGGINGFACE_API_KEY")
54
+ HF_IMAGE_MODEL = "stabilityai/stable-diffusion-2-1" # Popular image generation model
55
+
56
+ @login_manager.user_loader
57
+ def load_user(user_id):
58
+ """Load user for Flask-Login"""
59
+ return User.query.get(int(user_id))
60
+
61
+ @app.route('/')
62
+ def index():
63
+ """Main page - redirect to login if not authenticated"""
64
+ if current_user.is_authenticated:
65
+ return render_template('dashboard.html', user=current_user)
66
+ return render_template('index.html')
67
+
68
+ @app.route('/register', methods=['GET', 'POST'])
69
+ def register():
70
+ """User registration"""
71
+ if request.method == 'POST':
72
+ try:
73
+ data = request.get_json() if request.is_json else request.form
74
+ username = data.get('username', '').strip()
75
+ email = data.get('email', '').strip()
76
+ password = data.get('password', '').strip()
77
+
78
+ if not username or not email or not password:
79
+ return jsonify({'error': 'All fields are required'}), 400
80
+
81
+ # Check if user already exists
82
+ if User.query.filter_by(username=username).first():
83
+ return jsonify({'error': 'Username already exists'}), 400
84
+
85
+ if User.query.filter_by(email=email).first():
86
+ return jsonify({'error': 'Email already exists'}), 400
87
+
88
+ # Create new user
89
+ user = User(username=username, email=email)
90
+ user.set_password(password)
91
+
92
+ db.session.add(user)
93
+ db.session.commit()
94
+
95
+ # Log in the user
96
+ login_user(user)
97
+
98
+ if request.is_json:
99
+ return jsonify({'success': True, 'redirect': url_for('index')})
100
+ else:
101
+ flash('Registration successful! Welcome to InkBoard!', 'success')
102
+ return redirect(url_for('index'))
103
+
104
+ except Exception as e:
105
+ logging.error(f"Registration error: {str(e)}")
106
+ if request.is_json:
107
+ return jsonify({'error': 'Registration failed'}), 500
108
+ else:
109
+ flash('Registration failed. Please try again.', 'error')
110
+ return render_template('register.html')
111
+
112
+ return render_template('register.html')
113
+
114
+ @app.route('/login', methods=['GET', 'POST'])
115
+ def login():
116
+ """User login"""
117
+ if request.method == 'POST':
118
+ try:
119
+ data = request.get_json() if request.is_json else request.form
120
+ username = data.get('username', '').strip()
121
+ password = data.get('password', '').strip()
122
+
123
+ if not username or not password:
124
+ return jsonify({'error': 'Username and password are required'}), 400
125
+
126
+ # Find user by username or email
127
+ user = User.query.filter(
128
+ (User.username == username) | (User.email == username)
129
+ ).first()
130
+
131
+ if user and user.check_password(password):
132
+ login_user(user)
133
+ if request.is_json:
134
+ return jsonify({'success': True, 'redirect': url_for('index')})
135
+ else:
136
+ flash('Welcome back to InkBoard!', 'success')
137
+ return redirect(url_for('index'))
138
+ else:
139
+ if request.is_json:
140
+ return jsonify({'error': 'Invalid username or password'}), 401
141
+ else:
142
+ flash('Invalid username or password', 'error')
143
+ return render_template('login.html')
144
+
145
+ except Exception as e:
146
+ logging.error(f"Login error: {str(e)}")
147
+ if request.is_json:
148
+ return jsonify({'error': 'Login failed'}), 500
149
+ else:
150
+ flash('Login failed. Please try again.', 'error')
151
+ return render_template('login.html')
152
+
153
+ return render_template('login.html')
154
+
155
+ @app.route('/logout')
156
+ @login_required
157
+ def logout():
158
+ """User logout"""
159
+ logout_user()
160
+ flash('You have been logged out', 'info')
161
+ return redirect(url_for('index'))
162
+
163
+ @app.route('/generate', methods=['POST'])
164
+ @login_required
165
+ def generate_content():
166
+ """Generate story and image from scene description using Cohere and Hugging Face"""
167
+ try:
168
+ data = request.get_json()
169
+ scene_idea = data.get('scene_idea', '').strip()
170
+
171
+ if not scene_idea:
172
+ return jsonify({'error': 'Please provide a scene idea'}), 400
173
+
174
+ if not cohere_client:
175
+ return jsonify({'error': 'Cohere API key not configured'}), 500
176
+
177
+ # Generate expanded story using Cohere
178
+ story_prompt = f"""Transform this scene idea into a vivid, descriptive paragraph that paints a beautiful picture with words. Keep it between 80-150 words, rich in sensory details and atmosphere:
179
+
180
+ Scene idea: {scene_idea}
181
+
182
+ Write a single, flowing paragraph that brings this scene to life with beautiful imagery and emotions."""
183
+
184
+ logging.debug(f"Generating story for scene: {scene_idea}")
185
+
186
+ # Use Cohere's generate endpoint for text generation
187
+ story_response = cohere_client.generate(
188
+ model='command', # Cohere's flagship model
189
+ prompt=story_prompt,
190
+ max_tokens=200, # Limit to keep response concise (80-150 words)
191
+ temperature=0.7,
192
+ k=0,
193
+ stop_sequences=[],
194
+ return_likelihoods='NONE'
195
+ )
196
+
197
+ expanded_story = story_response.generations[0].text.strip()
198
+ logging.debug(f"Generated story: {expanded_story[:100]}...")
199
+
200
+ # Generate image using Hugging Face API
201
+ image_url = None
202
+ if HUGGINGFACE_API_KEY:
203
+ try:
204
+ image_url = generate_image_hf(scene_idea, expanded_story)
205
+ logging.debug(f"Generated image URL: {image_url}")
206
+ except Exception as img_error:
207
+ logging.warning(f"Image generation failed: {str(img_error)}")
208
+ # Continue without image - story generation is primary feature
209
+
210
+ # Save to database instead of session
211
+ creation_id = str(uuid.uuid4())
212
+ creation = Creation(
213
+ id=creation_id,
214
+ user_id=current_user.id,
215
+ scene_idea=scene_idea,
216
+ story=expanded_story,
217
+ image_url=image_url
218
+ )
219
+
220
+ db.session.add(creation)
221
+ db.session.commit()
222
+
223
+ return jsonify({
224
+ 'success': True,
225
+ 'story': expanded_story,
226
+ 'image_url': image_url,
227
+ 'creation_id': creation_id
228
+ })
229
+
230
+ except Exception as e:
231
+ logging.error(f"Error generating content: {str(e)}")
232
+ return jsonify({'error': f'An error occurred: {str(e)}'}), 500
233
+
234
+ def generate_image_hf(scene_idea, story):
235
+ """Generate image using Hugging Face API or create a beautiful SVG placeholder"""
236
+ try:
237
+ # First try Hugging Face API
238
+ if HUGGINGFACE_API_KEY:
239
+ image_prompt = f"A beautiful artistic illustration of: {scene_idea}. Style: dreamy, soft colors, high quality digital art, atmospheric"
240
+
241
+ # Try multiple models in case one fails
242
+ models_to_try = [
243
+ "runwayml/stable-diffusion-v1-5",
244
+ "stabilityai/stable-diffusion-2-1",
245
+ "CompVis/stable-diffusion-v1-4"
246
+ ]
247
+
248
+ for model in models_to_try:
249
+ try:
250
+ api_url = f"https://api-inference.huggingface.co/models/{model}"
251
+ headers = {"Authorization": f"Bearer {HUGGINGFACE_API_KEY}"}
252
+
253
+ # Send request to Hugging Face
254
+ response = requests.post(
255
+ api_url,
256
+ headers=headers,
257
+ json={"inputs": image_prompt},
258
+ timeout=60
259
+ )
260
+
261
+ if response.status_code == 200:
262
+ # Save the image temporarily and return a placeholder URL
263
+ # In a real app, you'd upload to cloud storage
264
+ import base64
265
+ image_data = response.content
266
+ image_base64 = base64.b64encode(image_data).decode('utf-8')
267
+ logging.debug(f"Successfully generated image with model: {model}")
268
+ return f"data:image/png;base64,{image_base64}"
269
+ else:
270
+ logging.warning(f"Model {model} failed: {response.status_code} - {response.text}")
271
+ continue
272
+
273
+ except Exception as model_error:
274
+ logging.warning(f"Model {model} error: {str(model_error)}")
275
+ continue
276
+
277
+ logging.warning("All Hugging Face models failed, falling back to SVG placeholder")
278
+
279
+ # Create a beautiful SVG placeholder based on scene description
280
+ return generate_svg_placeholder(scene_idea, story)
281
+
282
+ except Exception as e:
283
+ logging.error(f"Error generating image: {str(e)}")
284
+ return generate_svg_placeholder(scene_idea, story)
285
+
286
+ def generate_svg_placeholder(scene_idea, story):
287
+ """Generate a beautiful SVG placeholder image based on the scene"""
288
+ try:
289
+ # Create a color palette based on keywords in the scene
290
+ colors = get_scene_colors(scene_idea.lower())
291
+
292
+ # Create SVG with gradient background and artistic elements
293
+ svg_content = f"""
294
+ <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
295
+ <defs>
296
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
297
+ <stop offset="0%" style="stop-color:{colors['primary']};stop-opacity:1" />
298
+ <stop offset="100%" style="stop-color:{colors['secondary']};stop-opacity:1" />
299
+ </linearGradient>
300
+ <radialGradient id="glow" cx="50%" cy="50%" r="50%">
301
+ <stop offset="0%" style="stop-color:white;stop-opacity:0.3" />
302
+ <stop offset="100%" style="stop-color:white;stop-opacity:0" />
303
+ </radialGradient>
304
+ </defs>
305
+
306
+ <!-- Background -->
307
+ <rect width="400" height="400" fill="url(#bg)" />
308
+
309
+ <!-- Artistic elements based on scene -->
310
+ {get_scene_elements(scene_idea.lower(), colors)}
311
+
312
+ <!-- Glow effect -->
313
+ <rect width="400" height="400" fill="url(#glow)" />
314
+
315
+ <!-- Scene text -->
316
+ <text x="200" y="350" font-family="Arial, sans-serif" font-size="14" fill="white" text-anchor="middle" opacity="0.8">
317
+ {scene_idea[:40]}{"..." if len(scene_idea) > 40 else ""}
318
+ </text>
319
+ </svg>
320
+ """
321
+
322
+ # Convert SVG to base64 data URL
323
+ import base64
324
+ svg_base64 = base64.b64encode(svg_content.encode('utf-8')).decode('utf-8')
325
+ return f"data:image/svg+xml;base64,{svg_base64}"
326
+
327
+ except Exception as e:
328
+ logging.error(f"Error generating SVG placeholder: {str(e)}")
329
+ return None
330
+
331
+ def get_scene_colors(scene_text):
332
+ """Get color palette based on scene description"""
333
+ # Default colors
334
+ colors = {
335
+ 'primary': '#4fd1c7',
336
+ 'secondary': '#88d8f7',
337
+ 'accent': '#a8e6cf'
338
+ }
339
+
340
+ # Adjust colors based on keywords
341
+ if any(word in scene_text for word in ['sunset', 'dawn', 'orange', 'red']):
342
+ colors = {'primary': '#ff6b6b', 'secondary': '#ffa726', 'accent': '#ffcc80'}
343
+ elif any(word in scene_text for word in ['night', 'dark', 'moon', 'stars']):
344
+ colors = {'primary': '#3f51b5', 'secondary': '#1a237e', 'accent': '#7986cb'}
345
+ elif any(word in scene_text for word in ['forest', 'green', 'nature', 'tree']):
346
+ colors = {'primary': '#4caf50', 'secondary': '#2e7d32', 'accent': '#a5d6a7'}
347
+ elif any(word in scene_text for word in ['ocean', 'sea', 'water', 'blue']):
348
+ colors = {'primary': '#2196f3', 'secondary': '#0d47a1', 'accent': '#90caf9'}
349
+ elif any(word in scene_text for word in ['fire', 'flame', 'hot', 'warm']):
350
+ colors = {'primary': '#f44336', 'secondary': '#d32f2f', 'accent': '#ffab91'}
351
+
352
+ return colors
353
+
354
+ def get_scene_elements(scene_text, colors):
355
+ """Generate SVG elements based on scene description"""
356
+ elements = []
357
+
358
+ # Add different shapes and elements based on keywords
359
+ if any(word in scene_text for word in ['mountain', 'cliff', 'hill']):
360
+ elements.append(f'<polygon points="0,400 150,200 300,250 400,400" fill="{colors["accent"]}" opacity="0.7" />')
361
+ elements.append(f'<polygon points="100,400 250,150 400,200 400,400" fill="{colors["primary"]}" opacity="0.6" />')
362
+
363
+ if any(word in scene_text for word in ['sun', 'sunset', 'sunrise']):
364
+ elements.append(f'<circle cx="300" cy="100" r="40" fill="#ffeb3b" opacity="0.8" />')
365
+ elements.append(f'<circle cx="300" cy="100" r="60" fill="#fff59d" opacity="0.3" />')
366
+
367
+ if any(word in scene_text for word in ['moon', 'night']):
368
+ elements.append(f'<circle cx="320" cy="80" r="30" fill="#f5f5f5" opacity="0.9" />')
369
+ elements.append(f'<circle cx="100" cy="150" r="2" fill="white" opacity="0.8" />')
370
+ elements.append(f'<circle cx="150" cy="120" r="1.5" fill="white" opacity="0.7" />')
371
+ elements.append(f'<circle cx="200" cy="100" r="1" fill="white" opacity="0.6" />')
372
+
373
+ if any(word in scene_text for word in ['tree', 'forest']):
374
+ elements.append(f'<ellipse cx="80" cy="300" rx="15" ry="60" fill="{colors["accent"]}" opacity="0.8" />')
375
+ elements.append(f'<ellipse cx="120" cy="280" rx="20" ry="70" fill="{colors["primary"]}" opacity="0.7" />')
376
+
377
+ if any(word in scene_text for word in ['water', 'ocean', 'lake']):
378
+ elements.append(f'<ellipse cx="200" cy="350" rx="150" ry="30" fill="{colors["secondary"]}" opacity="0.6" />')
379
+ elements.append(f'<ellipse cx="200" cy="360" rx="180" ry="25" fill="{colors["primary"]}" opacity="0.4" />')
380
+
381
+ # Add some abstract artistic elements
382
+ elements.append(f'<circle cx="50" cy="80" r="8" fill="white" opacity="0.3" />')
383
+ elements.append(f'<circle cx="350" cy="300" r="12" fill="white" opacity="0.2" />')
384
+ elements.append(f'<circle cx="300" cy="250" r="6" fill="white" opacity="0.4" />')
385
+
386
+ return '\n'.join(elements)
387
+
388
+ @app.route('/save_journal', methods=['POST'])
389
+ @login_required
390
+ def save_journal():
391
+ """Save journal entry for a creation"""
392
+ try:
393
+ data = request.get_json()
394
+ creation_id = data.get('creation_id')
395
+ journal_entry = data.get('journal_entry', '').strip()
396
+
397
+ if not creation_id:
398
+ return jsonify({'error': 'Creation ID required'}), 400
399
+
400
+ # Find the creation in database
401
+ creation = Creation.query.filter_by(
402
+ id=creation_id,
403
+ user_id=current_user.id
404
+ ).first()
405
+
406
+ if not creation:
407
+ return jsonify({'error': 'Creation not found'}), 404
408
+
409
+ creation.journal_entry = journal_entry
410
+ creation.updated_at = datetime.utcnow()
411
+
412
+ db.session.commit()
413
+
414
+ return jsonify({'success': True})
415
+
416
+ except Exception as e:
417
+ logging.error(f"Error saving journal: {str(e)}")
418
+ return jsonify({'error': 'Failed to save journal entry'}), 500
419
+
420
+ @app.route('/get_creations')
421
+ @login_required
422
+ def get_creations():
423
+ """Get all user creations"""
424
+ try:
425
+ creations = Creation.query.filter_by(user_id=current_user.id).order_by(Creation.created_at.desc()).all()
426
+
427
+ creations_data = []
428
+ for creation in creations:
429
+ creations_data.append({
430
+ 'id': creation.id,
431
+ 'scene_idea': creation.scene_idea,
432
+ 'story': creation.story,
433
+ 'image_url': creation.image_url,
434
+ 'journal_entry': creation.journal_entry,
435
+ 'created_at': creation.created_at.isoformat()
436
+ })
437
+
438
+ return jsonify({'creations': creations_data})
439
+
440
+ except Exception as e:
441
+ logging.error(f"Error getting creations: {str(e)}")
442
+ return jsonify({'error': 'Failed to get creations'}), 500
443
+
444
+ # Import models after app and db are defined
445
+ from models import User, Creation
446
+
447
+ # Create database tables
448
+ with app.app_context():
449
+ db.create_all()
450
+ logging.info("Database tables created")
451
+
452
+ if __name__ == '__main__':
453
+ app.run(host='0.0.0.0', port=5000, debug=True)
main.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from app import app
2
+
3
+ if __name__ == '__main__':
4
+ app.run(host='0.0.0.0', port=5000, debug=True)
models.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from flask_login import UserMixin
4
+ from werkzeug.security import generate_password_hash, check_password_hash
5
+ from app import db
6
+
7
+ class User(UserMixin, db.Model):
8
+ """User model for authentication"""
9
+ __tablename__ = 'users'
10
+
11
+ id = db.Column(db.Integer, primary_key=True)
12
+ username = db.Column(db.String(80), unique=True, nullable=False)
13
+ email = db.Column(db.String(120), unique=True, nullable=False)
14
+ password_hash = db.Column(db.String(255), nullable=False)
15
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
16
+
17
+ # Relationship to creations
18
+ creations = db.relationship('Creation', backref='user', lazy=True, cascade='all, delete-orphan')
19
+
20
+ def set_password(self, password):
21
+ """Hash and set password"""
22
+ self.password_hash = generate_password_hash(password)
23
+
24
+ def check_password(self, password):
25
+ """Check if provided password matches hash"""
26
+ return check_password_hash(self.password_hash, password)
27
+
28
+ def __repr__(self):
29
+ return f'<User {self.username}>'
30
+
31
+ class Creation(db.Model):
32
+ """Model for user creations (stories and images)"""
33
+ __tablename__ = 'creations'
34
+
35
+ id = db.Column(db.String(36), primary_key=True) # UUID
36
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
37
+ scene_idea = db.Column(db.Text, nullable=False)
38
+ story = db.Column(db.Text, nullable=False)
39
+ image_url = db.Column(db.Text, nullable=True)
40
+ journal_entry = db.Column(db.Text, nullable=True)
41
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
42
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
43
+
44
+ def __repr__(self):
45
+ return f'<Creation {self.id[:8]}>'
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio
2
+ flask
3
+ cohere
4
+ transformers
5
+ requests
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.11.10
static/css/style.css ADDED
@@ -0,0 +1,766 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Root Variables - Purple Neon Pixelated Theme */
2
+ :root {
3
+ --neon-purple: #8B00FF;
4
+ --electric-pink: #FF00FF;
5
+ --cyber-blue: #00FFFF;
6
+ --dark-purple: #2E003E;
7
+ --pixel-black: #0a0a0a;
8
+ --pixel-gray: #1a1a1a;
9
+ --neon-green: #39FF14;
10
+ --shadow-neon: rgba(139, 0, 255, 0.5);
11
+ --shadow-glow: rgba(255, 0, 255, 0.3);
12
+ --gradient-cyber: linear-gradient(135deg, var(--neon-purple), var(--electric-pink));
13
+ --gradient-dark: linear-gradient(135deg, var(--dark-purple), var(--pixel-black));
14
+ --pixelated-border: 2px solid var(--neon-purple);
15
+ }
16
+
17
+ /* Global Styles */
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ font-family: 'Courier New', 'Monaco', monospace;
24
+ background: var(--pixel-black);
25
+ background-image:
26
+ radial-gradient(circle at 25% 25%, var(--neon-purple) 1px, transparent 1px),
27
+ radial-gradient(circle at 75% 75%, var(--electric-pink) 1px, transparent 1px);
28
+ background-size: 20px 20px;
29
+ min-height: 100vh;
30
+ color: var(--cyber-blue);
31
+ line-height: 1.6;
32
+ image-rendering: pixelated;
33
+ image-rendering: -moz-crisp-edges;
34
+ image-rendering: crisp-edges;
35
+ }
36
+
37
+ /* Typography */
38
+ h1, h2, h3, h4, h5, h6 {
39
+ font-family: 'Courier New', monospace;
40
+ font-weight: bold;
41
+ text-shadow: 0 0 10px currentColor;
42
+ letter-spacing: 2px;
43
+ }
44
+
45
+ .display-4 {
46
+ color: var(--neon-purple);
47
+ font-weight: bold;
48
+ text-transform: uppercase;
49
+ font-size: 3rem;
50
+ text-shadow:
51
+ 0 0 5px var(--neon-purple),
52
+ 0 0 10px var(--neon-purple),
53
+ 0 0 15px var(--neon-purple);
54
+ }
55
+
56
+ /* Header Styles */
57
+ header {
58
+ background: var(--gradient-dark);
59
+ background-attachment: fixed;
60
+ color: var(--cyber-blue);
61
+ border: var(--pixelated-border);
62
+ border-top: none;
63
+ border-left: none;
64
+ border-right: none;
65
+ margin-bottom: 2rem;
66
+ position: relative;
67
+ overflow: hidden;
68
+ }
69
+
70
+ header::before {
71
+ content: '';
72
+ position: absolute;
73
+ top: 0;
74
+ left: -100%;
75
+ width: 100%;
76
+ height: 100%;
77
+ background: linear-gradient(90deg, transparent, rgba(0, 255, 255, 0.2), transparent);
78
+ animation: scan 3s infinite;
79
+ }
80
+
81
+ @keyframes scan {
82
+ 0% { left: -100%; }
83
+ 100% { left: 100%; }
84
+ }
85
+
86
+ header h1 {
87
+ text-shadow:
88
+ 0 0 5px var(--neon-purple),
89
+ 0 0 10px var(--neon-purple),
90
+ 0 0 15px var(--neon-purple);
91
+ animation: glow 2s ease-in-out infinite alternate;
92
+ }
93
+
94
+ @keyframes glow {
95
+ from { text-shadow: 0 0 5px var(--neon-purple), 0 0 10px var(--neon-purple), 0 0 15px var(--neon-purple); }
96
+ to { text-shadow: 0 0 10px var(--electric-pink), 0 0 20px var(--electric-pink), 0 0 30px var(--electric-pink); }
97
+ }
98
+
99
+ /* Dashboard Navigation */
100
+ .dashboard-nav {
101
+ display: flex;
102
+ justify-content: center;
103
+ flex-wrap: wrap;
104
+ gap: 1rem;
105
+ }
106
+
107
+ .btn-nav {
108
+ border-radius: 25px;
109
+ padding: 0.6rem 1.5rem;
110
+ font-weight: 500;
111
+ transition: all 0.3s ease;
112
+ backdrop-filter: blur(10px);
113
+ border: 2px solid rgba(255, 255, 255, 0.3);
114
+ }
115
+
116
+ .btn-nav:hover {
117
+ background: rgba(255, 255, 255, 0.2);
118
+ border-color: rgba(255, 255, 255, 0.5);
119
+ transform: translateY(-2px);
120
+ }
121
+
122
+ .btn-nav.active {
123
+ background: rgba(255, 255, 255, 0.3);
124
+ border-color: rgba(255, 255, 255, 0.6);
125
+ }
126
+
127
+ /* Dashboard Sections */
128
+ .dashboard-section {
129
+ min-height: 400px;
130
+ animation: fadeIn 0.5s ease-in-out;
131
+ }
132
+
133
+ @keyframes fadeIn {
134
+ from { opacity: 0; transform: translateY(20px); }
135
+ to { opacity: 1; transform: translateY(0); }
136
+ }
137
+
138
+ /* Landing Page Styles */
139
+ .landing-body {
140
+ background: var(--pixel-black);
141
+ background-image:
142
+ radial-gradient(circle at 20% 20%, var(--neon-purple) 2px, transparent 2px),
143
+ radial-gradient(circle at 80% 80%, var(--electric-pink) 2px, transparent 2px),
144
+ radial-gradient(circle at 40% 60%, var(--cyber-blue) 1px, transparent 1px);
145
+ background-size: 30px 30px, 50px 50px, 20px 20px;
146
+ animation: pixelMove 10s linear infinite;
147
+ }
148
+
149
+ @keyframes pixelMove {
150
+ 0% { background-position: 0 0, 0 0, 0 0; }
151
+ 100% { background-position: 30px 30px, -50px 50px, -20px 20px; }
152
+ }
153
+
154
+ .cyber-title {
155
+ font-size: 4rem;
156
+ color: var(--neon-purple);
157
+ text-shadow:
158
+ 0 0 10px var(--neon-purple),
159
+ 0 0 20px var(--neon-purple),
160
+ 0 0 30px var(--neon-purple);
161
+ animation: titleGlow 3s ease-in-out infinite alternate;
162
+ }
163
+
164
+ @keyframes titleGlow {
165
+ from {
166
+ text-shadow: 0 0 10px var(--neon-purple), 0 0 20px var(--neon-purple), 0 0 30px var(--neon-purple);
167
+ transform: scale(1);
168
+ }
169
+ to {
170
+ text-shadow: 0 0 20px var(--electric-pink), 0 0 30px var(--electric-pink), 0 0 40px var(--electric-pink);
171
+ transform: scale(1.05);
172
+ }
173
+ }
174
+
175
+ .cyber-subtitle {
176
+ color: var(--cyber-blue);
177
+ font-family: 'Courier New', monospace;
178
+ font-size: 1.2rem;
179
+ letter-spacing: 1px;
180
+ text-shadow: 0 0 5px var(--cyber-blue);
181
+ }
182
+
183
+ .btn-outline-neon {
184
+ background: transparent;
185
+ border: 2px solid var(--cyber-blue);
186
+ color: var(--cyber-blue);
187
+ font-family: 'Courier New', monospace;
188
+ font-weight: bold;
189
+ text-transform: uppercase;
190
+ letter-spacing: 1px;
191
+ padding: 0.8rem 2rem;
192
+ transition: all 0.3s ease;
193
+ }
194
+
195
+ .btn-outline-neon:hover {
196
+ background: var(--cyber-blue);
197
+ color: var(--pixel-black);
198
+ box-shadow: 0 0 20px var(--cyber-blue);
199
+ transform: translateY(-2px);
200
+ }
201
+
202
+ .feature-box {
203
+ background: var(--pixel-gray);
204
+ border: 1px solid var(--neon-purple);
205
+ padding: 2rem;
206
+ transition: all 0.3s ease;
207
+ position: relative;
208
+ overflow: hidden;
209
+ }
210
+
211
+ .feature-box::before {
212
+ content: '';
213
+ position: absolute;
214
+ top: 0;
215
+ left: -100%;
216
+ width: 100%;
217
+ height: 100%;
218
+ background: linear-gradient(90deg, transparent, rgba(139, 0, 255, 0.1), transparent);
219
+ transition: left 0.5s;
220
+ }
221
+
222
+ .feature-box:hover::before {
223
+ left: 100%;
224
+ }
225
+
226
+ .feature-box:hover {
227
+ transform: translateY(-5px);
228
+ box-shadow: 0 0 20px var(--shadow-neon);
229
+ }
230
+
231
+ .feature-box i {
232
+ color: var(--neon-purple);
233
+ text-shadow: 0 0 10px var(--neon-purple);
234
+ }
235
+
236
+ /* Authentication Styles */
237
+ .auth-body {
238
+ background: var(--pixel-black);
239
+ background-image:
240
+ linear-gradient(45deg, var(--pixel-gray) 25%, transparent 25%),
241
+ linear-gradient(-45deg, var(--pixel-gray) 25%, transparent 25%),
242
+ linear-gradient(45deg, transparent 75%, var(--pixel-gray) 75%),
243
+ linear-gradient(-45deg, transparent 75%, var(--pixel-gray) 75%);
244
+ background-size: 20px 20px;
245
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
246
+ min-height: 100vh;
247
+ }
248
+
249
+ .auth-card {
250
+ background: var(--pixel-gray);
251
+ border: 3px solid var(--neon-purple);
252
+ border-radius: 0;
253
+ box-shadow:
254
+ 0 0 30px var(--shadow-neon),
255
+ inset 0 0 30px rgba(139, 0, 255, 0.1);
256
+ position: relative;
257
+ overflow: hidden;
258
+ }
259
+
260
+ .auth-card::before {
261
+ content: '';
262
+ position: absolute;
263
+ top: -50%;
264
+ left: -50%;
265
+ width: 200%;
266
+ height: 200%;
267
+ background: conic-gradient(from 0deg, var(--neon-purple), var(--electric-pink), var(--cyber-blue), var(--neon-purple));
268
+ animation: borderRotate 4s linear infinite;
269
+ z-index: -1;
270
+ }
271
+
272
+ @keyframes borderRotate {
273
+ 0% { transform: rotate(0deg); }
274
+ 100% { transform: rotate(360deg); }
275
+ }
276
+
277
+ .auth-title {
278
+ font-size: 2.5rem;
279
+ color: var(--neon-purple);
280
+ font-family: 'Courier New', monospace;
281
+ font-weight: bold;
282
+ text-shadow:
283
+ 0 0 10px var(--neon-purple),
284
+ 0 0 20px var(--neon-purple);
285
+ margin-bottom: 0.5rem;
286
+ }
287
+
288
+ .auth-subtitle {
289
+ color: var(--cyber-blue);
290
+ font-family: 'Courier New', monospace;
291
+ font-size: 0.9rem;
292
+ letter-spacing: 1px;
293
+ text-shadow: 0 0 5px var(--cyber-blue);
294
+ margin-bottom: 2rem;
295
+ }
296
+
297
+ .pixel-label {
298
+ color: var(--cyber-blue);
299
+ font-family: 'Courier New', monospace;
300
+ font-weight: bold;
301
+ text-transform: uppercase;
302
+ letter-spacing: 1px;
303
+ font-size: 0.9rem;
304
+ text-shadow: 0 0 5px var(--cyber-blue);
305
+ }
306
+
307
+ .pixel-input {
308
+ background: var(--pixel-black);
309
+ border: 2px solid var(--neon-purple);
310
+ border-radius: 0;
311
+ color: var(--cyber-blue);
312
+ font-family: 'Courier New', monospace;
313
+ padding: 0.8rem;
314
+ transition: all 0.3s ease;
315
+ }
316
+
317
+ .pixel-input:focus {
318
+ background: var(--pixel-black);
319
+ border-color: var(--electric-pink);
320
+ box-shadow: 0 0 20px var(--shadow-glow);
321
+ color: var(--cyber-blue);
322
+ }
323
+
324
+ .pixel-input::placeholder {
325
+ color: var(--cyber-blue);
326
+ opacity: 0.7;
327
+ }
328
+
329
+ .auth-link {
330
+ color: var(--cyber-blue);
331
+ font-family: 'Courier New', monospace;
332
+ font-size: 0.9rem;
333
+ letter-spacing: 1px;
334
+ }
335
+
336
+ .neon-link {
337
+ color: var(--neon-purple);
338
+ text-decoration: none;
339
+ font-weight: bold;
340
+ text-shadow: 0 0 5px var(--neon-purple);
341
+ transition: all 0.3s ease;
342
+ }
343
+
344
+ .neon-link:hover {
345
+ color: var(--electric-pink);
346
+ text-shadow: 0 0 10px var(--electric-pink);
347
+ text-decoration: none;
348
+ }
349
+
350
+ /* Card Styles */
351
+ .card {
352
+ border: var(--pixelated-border);
353
+ border-radius: 0;
354
+ transition: all 0.3s ease;
355
+ background: var(--pixel-gray);
356
+ backdrop-filter: blur(10px);
357
+ box-shadow:
358
+ 0 0 20px var(--shadow-neon),
359
+ inset 0 0 20px rgba(139, 0, 255, 0.1);
360
+ position: relative;
361
+ }
362
+
363
+ .card::before {
364
+ content: '';
365
+ position: absolute;
366
+ top: -2px;
367
+ left: -2px;
368
+ right: -2px;
369
+ bottom: -2px;
370
+ background: var(--gradient-cyber);
371
+ z-index: -1;
372
+ border-radius: 0;
373
+ }
374
+
375
+ .card:hover {
376
+ transform: translateY(-5px);
377
+ box-shadow:
378
+ 0 0 30px var(--shadow-glow),
379
+ 0 15px 35px var(--shadow-neon);
380
+ animation: pixelGlow 0.5s ease;
381
+ }
382
+
383
+ @keyframes pixelGlow {
384
+ 0%, 100% { box-shadow: 0 0 30px var(--shadow-glow); }
385
+ 50% { box-shadow: 0 0 50px var(--electric-pink); }
386
+ }
387
+
388
+ .input-card {
389
+ border: 3px solid var(--neon-purple);
390
+ background: var(--pixel-gray);
391
+ }
392
+
393
+ /* Form Styles */
394
+ .scene-input {
395
+ border-radius: 15px;
396
+ border: 2px solid var(--pastel-green);
397
+ padding: 1rem;
398
+ font-size: 1.1rem;
399
+ transition: all 0.3s ease;
400
+ background: var(--soft-white);
401
+ }
402
+
403
+ .scene-input:focus {
404
+ border-color: var(--primary-teal);
405
+ box-shadow: 0 0 0 0.2rem var(--shadow-light);
406
+ background: white;
407
+ }
408
+
409
+ /* Button Styles */
410
+ .animate-btn {
411
+ background: var(--gradient-cyber);
412
+ border: 2px solid var(--neon-purple);
413
+ border-radius: 0;
414
+ padding: 0.8rem 2rem;
415
+ font-weight: bold;
416
+ font-size: 1.1rem;
417
+ font-family: 'Courier New', monospace;
418
+ text-transform: uppercase;
419
+ letter-spacing: 1px;
420
+ transition: all 0.3s ease;
421
+ position: relative;
422
+ overflow: hidden;
423
+ min-width: 250px;
424
+ color: var(--pixel-black);
425
+ text-shadow: none;
426
+ }
427
+
428
+ .animate-btn::before {
429
+ content: '';
430
+ position: absolute;
431
+ top: 0;
432
+ left: -100%;
433
+ width: 100%;
434
+ height: 100%;
435
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
436
+ transition: left 0.5s;
437
+ }
438
+
439
+ .animate-btn:hover::before {
440
+ left: 100%;
441
+ }
442
+
443
+ .animate-btn:hover {
444
+ transform: translateY(-2px);
445
+ box-shadow:
446
+ 0 0 20px var(--neon-purple),
447
+ 0 10px 25px var(--shadow-neon);
448
+ background: var(--electric-pink);
449
+ border-color: var(--electric-pink);
450
+ }
451
+
452
+ .animate-btn:active {
453
+ transform: translateY(0);
454
+ animation: pixelPress 0.1s;
455
+ }
456
+
457
+ @keyframes pixelPress {
458
+ 0% { transform: scale(1); }
459
+ 50% { transform: scale(0.95); }
460
+ 100% { transform: scale(1); }
461
+ }
462
+
463
+ .animate-btn.loading {
464
+ pointer-events: none;
465
+ }
466
+
467
+ .animate-btn .btn-text {
468
+ transition: opacity 0.3s ease;
469
+ }
470
+
471
+ .animate-btn.loading .btn-text {
472
+ opacity: 0;
473
+ }
474
+
475
+ .animate-btn.loading .spinner-border {
476
+ display: inline-block !important;
477
+ position: absolute;
478
+ top: 50%;
479
+ left: 50%;
480
+ transform: translate(-50%, -50%);
481
+ }
482
+
483
+ /* Loading Animation */
484
+ .loading-animation {
485
+ display: flex;
486
+ justify-content: center;
487
+ gap: 0.5rem;
488
+ }
489
+
490
+ .spinner-grow {
491
+ animation-delay: 0s, 0.3s, 0.6s;
492
+ }
493
+
494
+ .spinner-grow:nth-child(1) { animation-delay: 0s; }
495
+ .spinner-grow:nth-child(2) { animation-delay: 0.3s; }
496
+ .spinner-grow:nth-child(3) { animation-delay: 0.6s; }
497
+
498
+ /* Pinterest-style Masonry Grid */
499
+ .masonry-grid {
500
+ columns: 1;
501
+ column-gap: 1.5rem;
502
+ margin-top: 2rem;
503
+ }
504
+
505
+ @media (min-width: 576px) {
506
+ .masonry-grid {
507
+ columns: 2;
508
+ }
509
+ }
510
+
511
+ @media (min-width: 768px) {
512
+ .masonry-grid {
513
+ columns: 2;
514
+ }
515
+ }
516
+
517
+ @media (min-width: 992px) {
518
+ .masonry-grid {
519
+ columns: 3;
520
+ }
521
+ }
522
+
523
+ @media (min-width: 1200px) {
524
+ .masonry-grid {
525
+ columns: 4;
526
+ }
527
+ }
528
+
529
+ /* Gallery Item Styles */
530
+ .gallery-item {
531
+ break-inside: avoid;
532
+ margin-bottom: 1.5rem;
533
+ background: white;
534
+ border-radius: 20px;
535
+ overflow: hidden;
536
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
537
+ transition: all 0.3s ease;
538
+ }
539
+
540
+ .gallery-item:hover {
541
+ transform: translateY(-5px);
542
+ box-shadow: 0 15px 35px var(--shadow-medium);
543
+ }
544
+
545
+ .gallery-item img {
546
+ width: 100%;
547
+ height: auto;
548
+ display: block;
549
+ border-radius: 15px 15px 0 0;
550
+ }
551
+
552
+ .gallery-item-content {
553
+ padding: 1.5rem;
554
+ }
555
+
556
+ .gallery-item-scene {
557
+ font-size: 0.9rem;
558
+ color: var(--medium-gray);
559
+ font-style: italic;
560
+ margin-bottom: 1rem;
561
+ padding: 0.5rem;
562
+ background: var(--light-gray);
563
+ border-radius: 10px;
564
+ border-left: 4px solid var(--primary-teal);
565
+ }
566
+
567
+ .gallery-item-story {
568
+ font-size: 0.95rem;
569
+ line-height: 1.6;
570
+ color: var(--dark-gray);
571
+ margin-bottom: 1rem;
572
+ }
573
+
574
+ .gallery-item-actions {
575
+ display: flex;
576
+ gap: 0.5rem;
577
+ margin-top: 1rem;
578
+ }
579
+
580
+ .btn-sm {
581
+ border-radius: 20px;
582
+ padding: 0.4rem 1rem;
583
+ font-size: 0.85rem;
584
+ }
585
+
586
+ .btn-outline-primary {
587
+ border-color: var(--primary-teal);
588
+ color: var(--primary-teal);
589
+ }
590
+
591
+ .btn-outline-primary:hover {
592
+ background: var(--primary-teal);
593
+ border-color: var(--primary-teal);
594
+ }
595
+
596
+ .btn-outline-success {
597
+ border-color: var(--pastel-green);
598
+ color: var(--pastel-green);
599
+ }
600
+
601
+ .btn-outline-success:hover {
602
+ background: var(--pastel-green);
603
+ border-color: var(--pastel-green);
604
+ }
605
+
606
+ /* Journal Styles */
607
+ .journal-entry {
608
+ background: var(--light-gray);
609
+ border-left: 4px solid var(--pastel-green);
610
+ padding: 1rem;
611
+ margin-top: 1rem;
612
+ border-radius: 0 10px 10px 0;
613
+ font-style: italic;
614
+ }
615
+
616
+ /* Modal Styles */
617
+ .modal-content {
618
+ border-radius: 20px;
619
+ border: none;
620
+ }
621
+
622
+ .modal-header {
623
+ background: var(--gradient-soft);
624
+ color: white;
625
+ border-radius: 20px 20px 0 0;
626
+ }
627
+
628
+ .modal-header .btn-close {
629
+ filter: brightness(0) invert(1);
630
+ }
631
+
632
+ /* Journal Entry Styles */
633
+ .journal-card {
634
+ border-radius: 15px;
635
+ margin-bottom: 1.5rem;
636
+ background: linear-gradient(135deg, var(--soft-white), var(--light-gray));
637
+ border-left: 4px solid var(--primary-teal);
638
+ }
639
+
640
+ .journal-entry-preview {
641
+ max-height: 150px;
642
+ overflow: hidden;
643
+ position: relative;
644
+ }
645
+
646
+ .journal-entry-preview::after {
647
+ content: '';
648
+ position: absolute;
649
+ bottom: 0;
650
+ left: 0;
651
+ right: 0;
652
+ height: 30px;
653
+ background: linear-gradient(transparent, white);
654
+ }
655
+
656
+ .journal-meta {
657
+ font-size: 0.85rem;
658
+ color: var(--medium-gray);
659
+ }
660
+
661
+ /* Responsive Design */
662
+ @media (max-width: 768px) {
663
+ .container {
664
+ padding: 0 1rem;
665
+ }
666
+
667
+ .scene-input {
668
+ font-size: 1rem;
669
+ }
670
+
671
+ .animate-btn {
672
+ min-width: 200px;
673
+ font-size: 1rem;
674
+ }
675
+
676
+ header {
677
+ padding: 2rem 0;
678
+ }
679
+
680
+ .display-4 {
681
+ font-size: 2rem;
682
+ }
683
+
684
+ .dashboard-nav {
685
+ flex-direction: column;
686
+ align-items: center;
687
+ }
688
+
689
+ .btn-nav {
690
+ width: 200px;
691
+ margin-bottom: 0.5rem;
692
+ }
693
+ }
694
+
695
+ /* Utility Classes */
696
+ .text-primary {
697
+ color: var(--primary-teal) !important;
698
+ }
699
+
700
+ .bg-primary {
701
+ background: var(--gradient-primary) !important;
702
+ }
703
+
704
+ .border-primary {
705
+ border-color: var(--primary-teal) !important;
706
+ }
707
+
708
+ /* Smooth Animations */
709
+ * {
710
+ transition: all 0.3s ease;
711
+ }
712
+
713
+ /* Focus Styles for Accessibility */
714
+ .btn:focus,
715
+ .form-control:focus {
716
+ outline: 2px solid var(--primary-teal);
717
+ outline-offset: 2px;
718
+ }
719
+
720
+ /* Loading States */
721
+ .loading-overlay {
722
+ position: fixed;
723
+ top: 0;
724
+ left: 0;
725
+ right: 0;
726
+ bottom: 0;
727
+ background: rgba(255, 255, 255, 0.9);
728
+ display: flex;
729
+ align-items: center;
730
+ justify-content: center;
731
+ z-index: 9999;
732
+ }
733
+
734
+ /* Custom Scrollbar */
735
+ ::-webkit-scrollbar {
736
+ width: 8px;
737
+ }
738
+
739
+ ::-webkit-scrollbar-track {
740
+ background: var(--light-gray);
741
+ }
742
+
743
+ ::-webkit-scrollbar-thumb {
744
+ background: var(--primary-teal);
745
+ border-radius: 4px;
746
+ }
747
+
748
+ ::-webkit-scrollbar-thumb:hover {
749
+ background: var(--light-blue);
750
+ }
751
+
752
+ /* Error Messages */
753
+ .alert {
754
+ border-radius: 15px;
755
+ border: none;
756
+ }
757
+
758
+ .alert-danger {
759
+ background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
760
+ color: white;
761
+ }
762
+
763
+ .alert-success {
764
+ background: linear-gradient(135deg, var(--pastel-green), var(--primary-teal));
765
+ color: white;
766
+ }
static/js/script.js ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // InkBoard JavaScript - Main Application Logic
2
+
3
+ class InkBoard {
4
+ constructor() {
5
+ this.currentCreationId = null;
6
+ this.currentSection = 'create';
7
+ this.init();
8
+ }
9
+
10
+ init() {
11
+ this.setupEventListeners();
12
+ this.loadGallery();
13
+ this.showSection('create'); // Default to create section
14
+ }
15
+
16
+ setupEventListeners() {
17
+ // Scene form submission
18
+ document.getElementById('scene-form').addEventListener('submit', (e) => {
19
+ e.preventDefault();
20
+ this.generateContent();
21
+ });
22
+
23
+ // Journal modal save button
24
+ document.getElementById('save-journal').addEventListener('click', () => {
25
+ this.saveJournal();
26
+ });
27
+
28
+ // Handle modal close
29
+ document.getElementById('journalModal').addEventListener('hidden.bs.modal', () => {
30
+ this.currentCreationId = null;
31
+ document.getElementById('journal-text').value = '';
32
+ });
33
+ }
34
+
35
+ async generateContent() {
36
+ const sceneIdea = document.getElementById('scene-idea').value.trim();
37
+
38
+ if (!sceneIdea) {
39
+ this.showAlert('Please enter a scene idea', 'danger');
40
+ return;
41
+ }
42
+
43
+ try {
44
+ this.showLoading(true);
45
+ this.setButtonLoading(true);
46
+
47
+ const response = await fetch('/generate', {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ },
52
+ body: JSON.stringify({
53
+ scene_idea: sceneIdea
54
+ })
55
+ });
56
+
57
+ const data = await response.json();
58
+
59
+ if (data.success) {
60
+ this.displayResult(data);
61
+ this.loadGallery(); // Refresh gallery
62
+ document.getElementById('scene-idea').value = ''; // Clear input
63
+ this.showAlert('Story and image generated successfully!', 'success');
64
+ } else {
65
+ this.showAlert(data.error || 'Failed to generate content', 'danger');
66
+ }
67
+
68
+ } catch (error) {
69
+ console.error('Error generating content:', error);
70
+ this.showAlert('Network error. Please try again.', 'danger');
71
+ } finally {
72
+ this.showLoading(false);
73
+ this.setButtonLoading(false);
74
+ }
75
+ }
76
+
77
+ displayResult(data) {
78
+ const resultsSection = document.getElementById('results-section');
79
+
80
+ // Build image section only if image was generated
81
+ const imageSection = data.image_url ? `
82
+ <div class="col-lg-6 mb-4">
83
+ <div class="image-container">
84
+ <img src="${data.image_url}" alt="Generated Scene" class="img-fluid rounded shadow">
85
+ </div>
86
+ </div>
87
+ ` : '';
88
+
89
+ // Adjust story column width based on whether image exists
90
+ const storyColClass = data.image_url ? 'col-lg-6' : 'col-12';
91
+
92
+ // Build download button only if image exists
93
+ const downloadButton = data.image_url ? `
94
+ <button class="btn btn-outline-success btn-sm" onclick="inkBoard.downloadImage('${data.image_url}')">
95
+ <i class="fas fa-download me-1"></i>
96
+ Download Image
97
+ </button>
98
+ ` : '';
99
+
100
+ const resultHTML = `
101
+ <div class="col-12 mb-5">
102
+ <div class="card shadow-lg border-0">
103
+ <div class="card-body p-4">
104
+ <h4 class="card-title text-center mb-4">
105
+ <i class="fas fa-sparkles me-2"></i>
106
+ Your Creation
107
+ </h4>
108
+ <div class="row">
109
+ ${imageSection}
110
+ <div class="${storyColClass}">
111
+ <div class="story-container">
112
+ <h5 class="mb-3">
113
+ <i class="fas fa-book-open me-2"></i>
114
+ Your Story
115
+ </h5>
116
+ <p class="story-text">${data.story}</p>
117
+ <div class="mt-3">
118
+ <button class="btn btn-outline-primary btn-sm me-2" onclick="inkBoard.openJournal('${data.creation_id}')">
119
+ <i class="fas fa-journal-whills me-1"></i>
120
+ Add Journal Entry
121
+ </button>
122
+ ${downloadButton}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ `;
131
+
132
+ resultsSection.innerHTML = resultHTML;
133
+
134
+ // Smooth scroll to results
135
+ resultsSection.scrollIntoView({ behavior: 'smooth' });
136
+ }
137
+
138
+ async loadGallery() {
139
+ try {
140
+ const response = await fetch('/get_creations');
141
+ const data = await response.json();
142
+
143
+ const galleryGrid = document.getElementById('gallery-grid');
144
+
145
+ if (data.creations && data.creations.length > 0) {
146
+ const galleryHTML = data.creations.map(creation => this.createGalleryItem(creation)).join('');
147
+ galleryGrid.innerHTML = galleryHTML;
148
+ } else {
149
+ galleryGrid.innerHTML = `
150
+ <div class="col-12 text-center py-5">
151
+ <i class="fas fa-palette fa-3x text-muted mb-3"></i>
152
+ <h5 class="text-muted">No creations yet</h5>
153
+ <p class="text-muted">Start by describing a scene above!</p>
154
+ </div>
155
+ `;
156
+ }
157
+ } catch (error) {
158
+ console.error('Error loading gallery:', error);
159
+ }
160
+ }
161
+
162
+ createGalleryItem(creation) {
163
+ const journalEntry = creation.journal_entry ?
164
+ `<div class="journal-entry">
165
+ <small class="text-muted">
166
+ <i class="fas fa-journal-whills me-1"></i>
167
+ Journal Entry:
168
+ </small>
169
+ <p class="mb-0 mt-1">${creation.journal_entry}</p>
170
+ </div>` : '';
171
+
172
+ // Only include image section if image exists
173
+ const imageSection = creation.image_url ?
174
+ `<img src="${creation.image_url}" alt="Scene: ${creation.scene_idea}" loading="lazy">` : '';
175
+
176
+ // Only include download button if image exists
177
+ const downloadButton = creation.image_url ?
178
+ `<button class="btn btn-outline-success btn-sm" onclick="inkBoard.downloadImage('${creation.image_url}')">
179
+ <i class="fas fa-download me-1"></i>
180
+ Download
181
+ </button>` : '';
182
+
183
+ return `
184
+ <div class="gallery-item">
185
+ ${imageSection}
186
+ <div class="gallery-item-content">
187
+ <div class="gallery-item-scene">
188
+ <i class="fas fa-quote-left me-1"></i>
189
+ ${creation.scene_idea}
190
+ </div>
191
+ <div class="gallery-item-story">
192
+ ${creation.story}
193
+ </div>
194
+ ${journalEntry}
195
+ <div class="gallery-item-actions">
196
+ <button class="btn btn-outline-primary btn-sm" onclick="inkBoard.openJournal('${creation.id}', '${creation.journal_entry || ''}')">
197
+ <i class="fas fa-journal-whills me-1"></i>
198
+ ${creation.journal_entry ? 'Edit' : 'Add'} Journal
199
+ </button>
200
+ ${downloadButton}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ `;
205
+ }
206
+
207
+ openJournal(creationId, existingText = '') {
208
+ this.currentCreationId = creationId;
209
+ document.getElementById('journal-text').value = existingText;
210
+
211
+ const modal = new bootstrap.Modal(document.getElementById('journalModal'));
212
+ modal.show();
213
+ }
214
+
215
+ async saveJournal() {
216
+ if (!this.currentCreationId) {
217
+ this.showAlert('No creation selected', 'danger');
218
+ return;
219
+ }
220
+
221
+ const journalText = document.getElementById('journal-text').value.trim();
222
+
223
+ try {
224
+ const response = await fetch('/save_journal', {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Content-Type': 'application/json',
228
+ },
229
+ body: JSON.stringify({
230
+ creation_id: this.currentCreationId,
231
+ journal_entry: journalText
232
+ })
233
+ });
234
+
235
+ const data = await response.json();
236
+
237
+ if (data.success) {
238
+ this.showAlert('Journal entry saved!', 'success');
239
+ this.loadGallery(); // Refresh gallery
240
+
241
+ // Close modal
242
+ const modal = bootstrap.Modal.getInstance(document.getElementById('journalModal'));
243
+ modal.hide();
244
+ } else {
245
+ this.showAlert(data.error || 'Failed to save journal', 'danger');
246
+ }
247
+
248
+ } catch (error) {
249
+ console.error('Error saving journal:', error);
250
+ this.showAlert('Network error. Please try again.', 'danger');
251
+ }
252
+ }
253
+
254
+ downloadImage(imageUrl) {
255
+ // Create a temporary link element to trigger download
256
+ const link = document.createElement('a');
257
+ link.href = imageUrl;
258
+ link.download = `inkboard-creation-${Date.now()}.png`;
259
+ link.target = '_blank';
260
+
261
+ // Trigger download
262
+ document.body.appendChild(link);
263
+ link.click();
264
+ document.body.removeChild(link);
265
+
266
+ this.showAlert('Image download started!', 'success');
267
+ }
268
+
269
+ showLoading(show) {
270
+ const loadingSection = document.getElementById('loading-section');
271
+ const resultsSection = document.getElementById('results-section');
272
+
273
+ if (show) {
274
+ loadingSection.classList.remove('d-none');
275
+ resultsSection.innerHTML = ''; // Clear previous results
276
+ } else {
277
+ loadingSection.classList.add('d-none');
278
+ }
279
+ }
280
+
281
+ setButtonLoading(loading) {
282
+ const btn = document.getElementById('generate-btn');
283
+ const btnText = btn.querySelector('.btn-text');
284
+ const spinner = btn.querySelector('.spinner-border');
285
+
286
+ if (loading) {
287
+ btn.classList.add('loading');
288
+ btn.disabled = true;
289
+ btnText.classList.add('d-none');
290
+ spinner.classList.remove('d-none');
291
+ } else {
292
+ btn.classList.remove('loading');
293
+ btn.disabled = false;
294
+ btnText.classList.remove('d-none');
295
+ spinner.classList.add('d-none');
296
+ }
297
+ }
298
+
299
+ showAlert(message, type) {
300
+ // Remove existing alerts
301
+ const existingAlert = document.querySelector('.alert');
302
+ if (existingAlert) {
303
+ existingAlert.remove();
304
+ }
305
+
306
+ // Create new alert
307
+ const alert = document.createElement('div');
308
+ alert.className = `alert alert-${type} alert-dismissible fade show`;
309
+ alert.innerHTML = `
310
+ ${message}
311
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
312
+ `;
313
+
314
+ // Insert at the top of the main container
315
+ const main = document.querySelector('main');
316
+ main.insertBefore(alert, main.firstChild);
317
+
318
+ // Auto-dismiss after 5 seconds
319
+ setTimeout(() => {
320
+ if (alert.parentNode) {
321
+ alert.remove();
322
+ }
323
+ }, 5000);
324
+ }
325
+
326
+ showSection(sectionName) {
327
+ // Hide all sections
328
+ document.querySelectorAll('.dashboard-section').forEach(section => {
329
+ section.classList.add('d-none');
330
+ });
331
+
332
+ // Remove active class from all nav buttons
333
+ document.querySelectorAll('.btn-nav').forEach(btn => {
334
+ btn.classList.remove('active');
335
+ });
336
+
337
+ // Show selected section
338
+ const targetSection = document.getElementById(`${sectionName}-section`);
339
+ if (targetSection) {
340
+ targetSection.classList.remove('d-none');
341
+ }
342
+
343
+ // Add active class to the correct button
344
+ const buttonSelectors = {
345
+ 'create': 'Create Story',
346
+ 'gallery': 'Gallery',
347
+ 'journal': 'Journal'
348
+ };
349
+
350
+ document.querySelectorAll('.btn-nav').forEach(btn => {
351
+ if (btn.textContent.trim().includes(buttonSelectors[sectionName])) {
352
+ btn.classList.add('active');
353
+ }
354
+ });
355
+
356
+ // Update current section
357
+ this.currentSection = sectionName;
358
+
359
+ // Load content based on section
360
+ if (sectionName === 'gallery') {
361
+ this.loadGallery();
362
+ } else if (sectionName === 'journal') {
363
+ this.loadJournalEntries();
364
+ }
365
+ }
366
+
367
+ async loadJournalEntries() {
368
+ try {
369
+ const response = await fetch('/get_creations');
370
+ const data = await response.json();
371
+
372
+ const journalContainer = document.getElementById('journal-entries');
373
+
374
+ // Filter creations that have journal entries
375
+ const entriesWithJournal = data.creations?.filter(creation => creation.journal_entry) || [];
376
+
377
+ if (entriesWithJournal.length > 0) {
378
+ const journalHTML = entriesWithJournal.map(creation => this.createJournalEntry(creation)).join('');
379
+ journalContainer.innerHTML = journalHTML;
380
+ } else {
381
+ journalContainer.innerHTML = `
382
+ <div class="col-12 text-center py-5">
383
+ <i class="fas fa-journal-whills fa-3x text-muted mb-3"></i>
384
+ <h5 class="text-muted">No journal entries yet</h5>
385
+ <p class="text-muted">Create a story and add journal entries to see them here!</p>
386
+ </div>
387
+ `;
388
+ }
389
+ } catch (error) {
390
+ console.error('Error loading journal entries:', error);
391
+ }
392
+ }
393
+
394
+ createJournalEntry(creation) {
395
+ const date = new Date().toLocaleDateString();
396
+ return `
397
+ <div class="col-lg-6 col-xl-4 mb-4">
398
+ <div class="card journal-card">
399
+ <div class="card-body">
400
+ <div class="journal-meta mb-2">
401
+ <small class="text-muted">
402
+ <i class="fas fa-calendar me-1"></i>
403
+ ${date}
404
+ </small>
405
+ </div>
406
+ <h6 class="card-title">
407
+ <i class="fas fa-quote-left me-1"></i>
408
+ ${creation.scene_idea}
409
+ </h6>
410
+ <div class="journal-entry-preview mb-3">
411
+ <p class="card-text">${creation.journal_entry}</p>
412
+ </div>
413
+ <div class="d-flex justify-content-between align-items-center">
414
+ <button class="btn btn-sm btn-outline-primary" onclick="inkBoard.openJournal('${creation.id}', '${creation.journal_entry}')">
415
+ <i class="fas fa-edit me-1"></i>
416
+ Edit
417
+ </button>
418
+ <button class="btn btn-sm btn-outline-secondary" onclick="inkBoard.viewFullStory('${creation.id}')">
419
+ <i class="fas fa-eye me-1"></i>
420
+ View Story
421
+ </button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+ `;
427
+ }
428
+
429
+ viewFullStory(creationId) {
430
+ // Switch to gallery and highlight the specific creation
431
+ this.showSection('gallery');
432
+ // You could add highlighting logic here
433
+ }
434
+ }
435
+
436
+ // Initialize the application
437
+ const inkBoard = new InkBoard();
438
+
439
+ // Additional utility functions
440
+ document.addEventListener('DOMContentLoaded', function() {
441
+ // Add smooth scrolling for all anchor links
442
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
443
+ anchor.addEventListener('click', function (e) {
444
+ e.preventDefault();
445
+ document.querySelector(this.getAttribute('href')).scrollIntoView({
446
+ behavior: 'smooth'
447
+ });
448
+ });
449
+ });
450
+
451
+ // Add loading animation to images
452
+ document.addEventListener('load', function(e) {
453
+ if (e.target.tagName === 'IMG') {
454
+ e.target.style.opacity = '0';
455
+ e.target.style.transition = 'opacity 0.3s ease';
456
+ setTimeout(() => {
457
+ e.target.style.opacity = '1';
458
+ }, 100);
459
+ }
460
+ }, true);
461
+ });
462
+
463
+ // Handle window resize for masonry grid
464
+ window.addEventListener('resize', function() {
465
+ // Trigger reflow for masonry grid
466
+ const grid = document.getElementById('gallery-grid');
467
+ if (grid) {
468
+ grid.style.columnCount = '';
469
+ grid.offsetHeight; // Force reflow
470
+ }
471
+ });
templates/dashboard.html ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dashboard - InkBoard</title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Google Fonts -->
12
+ <link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
13
+
14
+ <!-- Font Awesome Icons -->
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
16
+
17
+ <!-- Custom CSS -->
18
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
19
+ </head>
20
+ <body>
21
+ <div class="container-fluid">
22
+ <!-- Header -->
23
+ <header class="text-center py-4">
24
+ <div class="d-flex justify-content-between align-items-center">
25
+ <h1 class="display-4 fw-bold mb-0">
26
+ <i class="fas fa-terminal me-2"></i>
27
+ INKBOARD
28
+ </h1>
29
+ <div class="user-info">
30
+ <span class="me-3">Welcome, {{ user.username }}!</span>
31
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-neon btn-sm">
32
+ <i class="fas fa-sign-out-alt me-2"></i>
33
+ LOGOUT
34
+ </a>
35
+ </div>
36
+ </div>
37
+ <p class="lead cyber-subtitle">// NEURAL STORY GENERATOR //</p>
38
+
39
+ <!-- Dashboard Navigation -->
40
+ <div class="dashboard-nav mt-4">
41
+ <button class="btn btn-outline-light btn-nav me-2 active" onclick="inkBoard.showSection('create')">
42
+ <i class="fas fa-plus-circle me-2"></i>
43
+ CREATE STORY
44
+ </button>
45
+ <button class="btn btn-outline-light btn-nav me-2" onclick="inkBoard.showSection('gallery')">
46
+ <i class="fas fa-palette me-2"></i>
47
+ GALLERY
48
+ </button>
49
+ <button class="btn btn-outline-light btn-nav" onclick="inkBoard.showSection('journal')">
50
+ <i class="fas fa-journal-whills me-2"></i>
51
+ JOURNAL
52
+ </button>
53
+ </div>
54
+ </header>
55
+
56
+ <!-- Main Content -->
57
+ <main class="container">
58
+ <!-- Create Story Section -->
59
+ <div id="create-section" class="dashboard-section">
60
+ <div class="row justify-content-center">
61
+ <div class="col-lg-8">
62
+ <div class="card input-card">
63
+ <div class="card-body p-4">
64
+ <h2 class="card-title text-center mb-4">
65
+ <i class="fas fa-magic me-2"></i>
66
+ CREATE YOUR STORY
67
+ </h2>
68
+
69
+ <form id="scene-form" class="needs-validation" novalidate>
70
+ <div class="mb-4">
71
+ <label for="scene-input" class="form-label pixel-label">
72
+ <i class="fas fa-lightbulb me-2"></i>
73
+ SCENE DESCRIPTION
74
+ </label>
75
+ <textarea
76
+ class="form-control pixel-input scene-input"
77
+ id="scene-input"
78
+ rows="3"
79
+ placeholder="Describe your scene... (e.g., a lonely lighthouse on a stormy night)"
80
+ required>
81
+ </textarea>
82
+ <div class="invalid-feedback">
83
+ Please describe your scene idea.
84
+ </div>
85
+ </div>
86
+
87
+ <div class="text-center">
88
+ <button type="submit" class="animate-btn" id="generate-btn">
89
+ <span class="btn-text">
90
+ <i class="fas fa-wand-magic-sparkles me-2"></i>
91
+ GENERATE STORY
92
+ </span>
93
+ <div class="spinner-border spinner-border-sm d-none ms-2" role="status"></div>
94
+ </button>
95
+ </div>
96
+ </form>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- Result Display -->
103
+ <div id="result-container" class="mt-4 d-none">
104
+ <div class="row">
105
+ <div class="col-lg-8 mx-auto">
106
+ <div class="card result-card">
107
+ <div class="card-body">
108
+ <h3 class="card-title">
109
+ <i class="fas fa-scroll me-2"></i>
110
+ YOUR STORY
111
+ </h3>
112
+ <div id="story-content" class="story-text mb-4"></div>
113
+
114
+ <div class="row">
115
+ <div class="col-md-6">
116
+ <div id="image-container" class="image-container mb-3">
117
+ <!-- Generated image will appear here -->
118
+ </div>
119
+ </div>
120
+ <div class="col-md-6">
121
+ <div class="story-actions">
122
+ <button class="btn btn-outline-neon btn-sm mb-2" onclick="inkBoard.openJournal()">
123
+ <i class="fas fa-journal-whills me-2"></i>
124
+ ADD TO JOURNAL
125
+ </button>
126
+ <button class="btn btn-outline-neon btn-sm" onclick="inkBoard.downloadImage()">
127
+ <i class="fas fa-download me-2"></i>
128
+ DOWNLOAD IMAGE
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Gallery Section -->
141
+ <div id="gallery-section" class="dashboard-section d-none">
142
+ <div class="row">
143
+ <div class="col-12">
144
+ <h2 class="text-center mb-4">
145
+ <i class="fas fa-palette me-2"></i>
146
+ YOUR GALLERY
147
+ </h2>
148
+ <div id="gallery-grid" class="row">
149
+ <!-- Gallery items will be loaded here -->
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Journal Section -->
156
+ <div id="journal-section" class="dashboard-section d-none">
157
+ <div class="row">
158
+ <div class="col-12">
159
+ <h2 class="text-center mb-4">
160
+ <i class="fas fa-journal-whills me-2"></i>
161
+ YOUR JOURNAL
162
+ </h2>
163
+ <div id="journal-entries" class="row">
164
+ <!-- Journal entries will be loaded here -->
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </main>
170
+ </div>
171
+
172
+ <!-- Journal Modal -->
173
+ <div class="modal fade" id="journalModal" tabindex="-1">
174
+ <div class="modal-dialog modal-lg">
175
+ <div class="modal-content">
176
+ <div class="modal-header">
177
+ <h5 class="modal-title">
178
+ <i class="fas fa-journal-whills me-2"></i>
179
+ ADD JOURNAL ENTRY
180
+ </h5>
181
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
182
+ </div>
183
+ <div class="modal-body">
184
+ <form id="journal-form">
185
+ <div class="mb-3">
186
+ <label for="journal-text" class="form-label pixel-label">YOUR THOUGHTS</label>
187
+ <textarea
188
+ class="form-control pixel-input"
189
+ id="journal-text"
190
+ rows="5"
191
+ placeholder="Write your thoughts about this story...">
192
+ </textarea>
193
+ </div>
194
+ <div class="text-end">
195
+ <button type="button" class="btn btn-outline-neon me-2" data-bs-dismiss="modal">
196
+ CANCEL
197
+ </button>
198
+ <button type="submit" class="btn animate-btn">
199
+ <i class="fas fa-save me-2"></i>
200
+ SAVE ENTRY
201
+ </button>
202
+ </div>
203
+ </form>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- Bootstrap JS -->
210
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
211
+
212
+ <!-- Custom JS -->
213
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
214
+
215
+ <script>
216
+ // Initialize InkBoard when document is ready
217
+ document.addEventListener('DOMContentLoaded', function() {
218
+ window.inkBoard = new InkBoard();
219
+ });
220
+ </script>
221
+ </body>
222
+ </html>
templates/index.html ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>InkBoard - Neural Story Generator</title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Google Fonts -->
12
+ <link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
13
+
14
+ <!-- Font Awesome Icons -->
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
16
+
17
+ <!-- Custom CSS -->
18
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
19
+ </head>
20
+ <body class="landing-body">
21
+ <div class="container-fluid h-100">
22
+ <div class="row h-100 justify-content-center align-items-center">
23
+ <div class="col-lg-8 text-center">
24
+ <!-- Main Landing Content -->
25
+ <div class="landing-content">
26
+ <h1 class="display-1 mb-4 cyber-title">
27
+ <i class="fas fa-terminal me-3"></i>
28
+ INKBOARD
29
+ </h1>
30
+ <p class="lead cyber-subtitle mb-5">
31
+ // NEURAL STORY GENERATOR //
32
+ <br>
33
+ Transform ideas into pixelated dreams
34
+ </p>
35
+
36
+ <!-- CTA Buttons -->
37
+ <div class="landing-actions">
38
+ <a href="{{ url_for('login') }}" class="btn animate-btn btn-lg me-3 mb-3">
39
+ <i class="fas fa-sign-in-alt me-2"></i>
40
+ LOGIN
41
+ </a>
42
+ <a href="{{ url_for('register') }}" class="btn btn-outline-neon btn-lg mb-3">
43
+ <i class="fas fa-user-plus me-2"></i>
44
+ CREATE ACCOUNT
45
+ </a>
46
+ </div>
47
+
48
+ <!-- Features -->
49
+ <div class="row mt-5">
50
+ <div class="col-md-4 mb-4">
51
+ <div class="feature-box">
52
+ <i class="fas fa-brain fa-3x mb-3"></i>
53
+ <h4>AI POWERED</h4>
54
+ <p>Advanced neural networks generate unique stories from your ideas</p>
55
+ </div>
56
+ </div>
57
+ <div class="col-md-4 mb-4">
58
+ <div class="feature-box">
59
+ <i class="fas fa-image fa-3x mb-3"></i>
60
+ <h4>VISUAL ART</h4>
61
+ <p>Each story comes with a custom generated pixelated illustration</p>
62
+ </div>
63
+ </div>
64
+ <div class="col-md-4 mb-4">
65
+ <div class="feature-box">
66
+ <i class="fas fa-journal-whills fa-3x mb-3"></i>
67
+ <h4>DIGITAL JOURNAL</h4>
68
+ <p>Save and organize your creative journey in your personal gallery</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Bootstrap JS -->
78
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
79
+ </body>
80
+ </html>
81
+ <main class="container">
82
+ <!-- Create Story Section -->
83
+ <div id="create-section" class="dashboard-section">
84
+ <!-- Scene Input Section -->
85
+ <div class="row justify-content-center mb-5">
86
+ <div class="col-lg-8">
87
+ <div class="card shadow-sm border-0 input-card">
88
+ <div class="card-body p-4">
89
+ <h3 class="card-title text-center mb-4">
90
+ <i class="fas fa-lightbulb me-2"></i>
91
+ Describe Your Scene
92
+ </h3>
93
+ <form id="scene-form">
94
+ <div class="mb-3">
95
+ <textarea
96
+ class="form-control scene-input"
97
+ id="scene-idea"
98
+ rows="4"
99
+ placeholder="A girl standing alone on a cliff at sunset..."
100
+ required></textarea>
101
+ </div>
102
+ <div class="text-center">
103
+ <button type="submit" class="btn btn-primary btn-lg animate-btn" id="generate-btn">
104
+ <span class="btn-text">
105
+ <i class="fas fa-magic me-2"></i>
106
+ Create Story & Image
107
+ </span>
108
+ <div class="spinner-border spinner-border-sm d-none" role="status">
109
+ <span class="visually-hidden">Loading...</span>
110
+ </div>
111
+ </button>
112
+ </div>
113
+ </form>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Loading Animation -->
120
+ <div class="row justify-content-center d-none" id="loading-section">
121
+ <div class="col-lg-8">
122
+ <div class="text-center py-5">
123
+ <div class="loading-animation mb-4">
124
+ <div class="spinner-grow text-primary" role="status"></div>
125
+ <div class="spinner-grow text-info" role="status"></div>
126
+ <div class="spinner-grow text-success" role="status"></div>
127
+ </div>
128
+ <h4 class="text-muted">Creating your story and image...</h4>
129
+ <p class="text-muted">This may take a moment</p>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Results Section -->
135
+ <div class="row" id="results-section">
136
+ <!-- Generated content will be inserted here -->
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Gallery Section -->
141
+ <div id="gallery-section" class="dashboard-section d-none">
142
+ <div class="row">
143
+ <div class="col-12">
144
+ <h3 class="text-center mb-4">
145
+ <i class="fas fa-palette me-2"></i>
146
+ Your Creative Gallery
147
+ </h3>
148
+ <div class="masonry-grid" id="gallery-grid">
149
+ <!-- Gallery items will be inserted here -->
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Journal Section -->
156
+ <div id="journal-section" class="dashboard-section d-none">
157
+ <div class="row">
158
+ <div class="col-12">
159
+ <h3 class="text-center mb-4">
160
+ <i class="fas fa-journal-whills me-2"></i>
161
+ Your Journal Entries
162
+ </h3>
163
+ <div class="row" id="journal-entries">
164
+ <!-- Journal entries will be inserted here -->
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </main>
170
+ </div>
171
+
172
+ <!-- Journal Modal -->
173
+ <div class="modal fade" id="journalModal" tabindex="-1" aria-labelledby="journalModalLabel" aria-hidden="true">
174
+ <div class="modal-dialog modal-lg">
175
+ <div class="modal-content">
176
+ <div class="modal-header">
177
+ <h5 class="modal-title" id="journalModalLabel">
178
+ <i class="fas fa-journal-whills me-2"></i>
179
+ Journal Entry
180
+ </h5>
181
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
182
+ </div>
183
+ <div class="modal-body">
184
+ <textarea class="form-control" id="journal-text" rows="6" placeholder="Write your thoughts, inspiration, or notes about this creation..."></textarea>
185
+ </div>
186
+ <div class="modal-footer">
187
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
188
+ <button type="button" class="btn btn-primary" id="save-journal">
189
+ <i class="fas fa-save me-2"></i>
190
+ Save Journal
191
+ </button>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Bootstrap JS -->
198
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
199
+
200
+ <!-- Custom JS -->
201
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
202
+ </body>
203
+ </html>
templates/login.html ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Login - InkBoard</title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Google Fonts -->
12
+ <link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
13
+
14
+ <!-- Font Awesome Icons -->
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
16
+
17
+ <!-- Custom CSS -->
18
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
19
+ </head>
20
+ <body class="auth-body">
21
+ <div class="container-fluid h-100">
22
+ <div class="row h-100 justify-content-center align-items-center">
23
+ <div class="col-md-6 col-lg-4">
24
+ <div class="auth-card card">
25
+ <div class="card-body p-5">
26
+ <!-- Logo/Title -->
27
+ <div class="text-center mb-4">
28
+ <h1 class="auth-title">
29
+ <i class="fas fa-terminal me-2"></i>
30
+ INKBOARD
31
+ </h1>
32
+ <p class="auth-subtitle">// NEURAL STORY GENERATOR //</p>
33
+ </div>
34
+
35
+ <!-- Login Form -->
36
+ <form id="login-form">
37
+ <div class="mb-3">
38
+ <label for="username" class="form-label pixel-label">
39
+ <i class="fas fa-user me-2"></i>USERNAME
40
+ </label>
41
+ <input type="text" class="form-control pixel-input" id="username" name="username" required>
42
+ </div>
43
+
44
+ <div class="mb-4">
45
+ <label for="password" class="form-label pixel-label">
46
+ <i class="fas fa-lock me-2"></i>PASSWORD
47
+ </label>
48
+ <input type="password" class="form-control pixel-input" id="password" name="password" required>
49
+ </div>
50
+
51
+ <button type="submit" class="btn animate-btn w-100 mb-3" id="login-btn">
52
+ <span class="btn-text">
53
+ <i class="fas fa-sign-in-alt me-2"></i>
54
+ LOGIN
55
+ </span>
56
+ <div class="spinner-border spinner-border-sm d-none" role="status"></div>
57
+ </button>
58
+ </form>
59
+
60
+ <!-- Register Link -->
61
+ <div class="text-center">
62
+ <p class="auth-link">
63
+ NEW USER?
64
+ <a href="{{ url_for('register') }}" class="neon-link">CREATE ACCOUNT</a>
65
+ </p>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Bootstrap JS -->
74
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
75
+
76
+ <!-- Auth JS -->
77
+ <script>
78
+ document.getElementById('login-form').addEventListener('submit', async (e) => {
79
+ e.preventDefault();
80
+
81
+ const btn = document.getElementById('login-btn');
82
+ const btnText = btn.querySelector('.btn-text');
83
+ const spinner = btn.querySelector('.spinner-border');
84
+
85
+ // Show loading
86
+ btn.disabled = true;
87
+ btnText.classList.add('d-none');
88
+ spinner.classList.remove('d-none');
89
+
90
+ try {
91
+ const formData = new FormData(e.target);
92
+ const response = await fetch('/login', {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify({
98
+ username: formData.get('username'),
99
+ password: formData.get('password')
100
+ })
101
+ });
102
+
103
+ const data = await response.json();
104
+
105
+ if (data.success) {
106
+ window.location.href = data.redirect;
107
+ } else {
108
+ showAlert(data.error || 'Login failed', 'danger');
109
+ }
110
+ } catch (error) {
111
+ showAlert('Network error. Please try again.', 'danger');
112
+ } finally {
113
+ // Hide loading
114
+ btn.disabled = false;
115
+ btnText.classList.remove('d-none');
116
+ spinner.classList.add('d-none');
117
+ }
118
+ });
119
+
120
+ function showAlert(message, type) {
121
+ const existingAlert = document.querySelector('.alert');
122
+ if (existingAlert) existingAlert.remove();
123
+
124
+ const alert = document.createElement('div');
125
+ alert.className = `alert alert-${type} alert-dismissible fade show`;
126
+ alert.innerHTML = `
127
+ ${message}
128
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
129
+ `;
130
+
131
+ document.querySelector('.card-body').insertBefore(alert, document.querySelector('form'));
132
+
133
+ setTimeout(() => {
134
+ if (alert.parentNode) alert.remove();
135
+ }, 5000);
136
+ }
137
+ </script>
138
+ </body>
139
+ </html>
templates/register.html ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Register - InkBoard</title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Google Fonts -->
12
+ <link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
13
+
14
+ <!-- Font Awesome Icons -->
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
16
+
17
+ <!-- Custom CSS -->
18
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
19
+ </head>
20
+ <body class="auth-body">
21
+ <div class="container-fluid h-100">
22
+ <div class="row h-100 justify-content-center align-items-center">
23
+ <div class="col-md-6 col-lg-4">
24
+ <div class="auth-card card">
25
+ <div class="card-body p-5">
26
+ <!-- Logo/Title -->
27
+ <div class="text-center mb-4">
28
+ <h1 class="auth-title">
29
+ <i class="fas fa-terminal me-2"></i>
30
+ INKBOARD
31
+ </h1>
32
+ <p class="auth-subtitle">// CREATE ACCOUNT //</p>
33
+ </div>
34
+
35
+ <!-- Register Form -->
36
+ <form id="register-form">
37
+ <div class="mb-3">
38
+ <label for="username" class="form-label pixel-label">
39
+ <i class="fas fa-user me-2"></i>USERNAME
40
+ </label>
41
+ <input type="text" class="form-control pixel-input" id="username" name="username" required>
42
+ </div>
43
+
44
+ <div class="mb-3">
45
+ <label for="email" class="form-label pixel-label">
46
+ <i class="fas fa-envelope me-2"></i>EMAIL
47
+ </label>
48
+ <input type="email" class="form-control pixel-input" id="email" name="email" required>
49
+ </div>
50
+
51
+ <div class="mb-4">
52
+ <label for="password" class="form-label pixel-label">
53
+ <i class="fas fa-lock me-2"></i>PASSWORD
54
+ </label>
55
+ <input type="password" class="form-control pixel-input" id="password" name="password" required>
56
+ </div>
57
+
58
+ <button type="submit" class="btn animate-btn w-100 mb-3" id="register-btn">
59
+ <span class="btn-text">
60
+ <i class="fas fa-user-plus me-2"></i>
61
+ CREATE ACCOUNT
62
+ </span>
63
+ <div class="spinner-border spinner-border-sm d-none" role="status"></div>
64
+ </button>
65
+ </form>
66
+
67
+ <!-- Login Link -->
68
+ <div class="text-center">
69
+ <p class="auth-link">
70
+ ALREADY HAVE AN ACCOUNT?
71
+ <a href="{{ url_for('login') }}" class="neon-link">LOGIN</a>
72
+ </p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Bootstrap JS -->
81
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
82
+
83
+ <!-- Auth JS -->
84
+ <script>
85
+ document.getElementById('register-form').addEventListener('submit', async (e) => {
86
+ e.preventDefault();
87
+
88
+ const btn = document.getElementById('register-btn');
89
+ const btnText = btn.querySelector('.btn-text');
90
+ const spinner = btn.querySelector('.spinner-border');
91
+
92
+ // Show loading
93
+ btn.disabled = true;
94
+ btnText.classList.add('d-none');
95
+ spinner.classList.remove('d-none');
96
+
97
+ try {
98
+ const formData = new FormData(e.target);
99
+ const response = await fetch('/register', {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ },
104
+ body: JSON.stringify({
105
+ username: formData.get('username'),
106
+ email: formData.get('email'),
107
+ password: formData.get('password')
108
+ })
109
+ });
110
+
111
+ const data = await response.json();
112
+
113
+ if (data.success) {
114
+ window.location.href = data.redirect;
115
+ } else {
116
+ showAlert(data.error || 'Registration failed', 'danger');
117
+ }
118
+ } catch (error) {
119
+ showAlert('Network error. Please try again.', 'danger');
120
+ } finally {
121
+ // Hide loading
122
+ btn.disabled = false;
123
+ btnText.classList.remove('d-none');
124
+ spinner.classList.add('d-none');
125
+ }
126
+ });
127
+
128
+ function showAlert(message, type) {
129
+ const existingAlert = document.querySelector('.alert');
130
+ if (existingAlert) existingAlert.remove();
131
+
132
+ const alert = document.createElement('div');
133
+ alert.className = `alert alert-${type} alert-dismissible fade show`;
134
+ alert.innerHTML = `
135
+ ${message}
136
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
137
+ `;
138
+
139
+ document.querySelector('.card-body').insertBefore(alert, document.querySelector('form'));
140
+
141
+ setTimeout(() => {
142
+ if (alert.parentNode) alert.remove();
143
+ }, 5000);
144
+ }
145
+ </script>
146
+ </body>
147
+ </html>