Website live
This commit is contained in:
commit
24e27adb8f
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Created by venv; see https://docs.python.org/3/library/venv.html
|
||||||
|
__pycache__
|
||||||
|
bin/
|
||||||
|
include/
|
||||||
|
lib/
|
||||||
|
pyvenv.cfg
|
||||||
|
|
||||||
|
images/
|
||||||
|
files_snapshot.txt
|
||||||
89
README.md
Normal file
89
README.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Space Facts Viewer
|
||||||
|
|
||||||
|
A mobile-optimized picture viewer app that displays space images with interesting facts about each image. Click on an image to toggle the visibility of the associated fact.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Full-screen image display with overlaying facts
|
||||||
|
- Toggle functionality - click image to show/hide facts
|
||||||
|
- Previous/Next navigation buttons
|
||||||
|
- Mobile-responsive design optimized for touch interaction
|
||||||
|
- Clean, space-themed dark interface
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install Python Flask:
|
||||||
|
```
|
||||||
|
pip install flask
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure you have the required directory structure:
|
||||||
|
```
|
||||||
|
SpaceFacts/
|
||||||
|
├── app.py
|
||||||
|
├── requirements.txt
|
||||||
|
├── templates/
|
||||||
|
│ └── index.html
|
||||||
|
├── static/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── style.css
|
||||||
|
│ ├── js/
|
||||||
|
│ │ └── script.js
|
||||||
|
│ └── images/ # Placeholder images already included
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. The app is already configured to use the placeholder images in the `static/images/` directory:
|
||||||
|
- `20250331-04~orig.jpg`
|
||||||
|
- `AFRC2025-0036-26~orig.jpg`
|
||||||
|
- `CEB_1408~orig.jpg`
|
||||||
|
- `iss072e742575~orig.jpg`
|
||||||
|
- `iss073e0000706~orig.jpg`
|
||||||
|
|
||||||
|
2. Run the Flask application:
|
||||||
|
```
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open your browser and navigate to `http://localhost:6007`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
- **Click on any image** to toggle the visibility of the fact overlay
|
||||||
|
- Use the **Previous/Next buttons** to navigate between different space facts
|
||||||
|
- The interface is fully responsive and optimized for mobile devices
|
||||||
|
- Images are displayed in full-screen mode with facts overlaid at the bottom
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Backend (Flask)
|
||||||
|
The Flask app provides REST API endpoints:
|
||||||
|
- `/` - Main application page
|
||||||
|
- `/api/facts` - JSON list of all space facts
|
||||||
|
- `/api/fact/<id>` - JSON for a specific fact
|
||||||
|
- `/api/image/<id>` - Returns the image file for a fact
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Responsive design using CSS media queries
|
||||||
|
- JavaScript handles click events and navigation
|
||||||
|
- Smooth slide animations for overlay toggling
|
||||||
|
- Full-screen canvas with proper aspect ratio handling
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
To add more facts:
|
||||||
|
1. Add entries to the `SPACE_FACTS` array in `app.py`
|
||||||
|
2. Ensure corresponding image files exist in `static/images/`
|
||||||
|
|
||||||
|
## Mobile Optimization
|
||||||
|
|
||||||
|
The application is optimized for mobile devices with:
|
||||||
|
- Touch-friendly button sizes
|
||||||
|
- Responsive layout adjustments
|
||||||
|
- Single-tap interaction for toggling facts
|
||||||
|
- Optimized viewport settings
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is built for educational purposes and can be modified freely.
|
||||||
244
app.py
Normal file
244
app.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
from flask import Flask, render_template, send_file, jsonify
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import json
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Configuration for NASA data paths - using environment variables with defaults
|
||||||
|
METADATA_PATH = os.environ.get(
|
||||||
|
"NASA_METADATA_PATH", "/Volumes/MediaServer/Images/NASA/metadata"
|
||||||
|
)
|
||||||
|
IMAGES_PATH = os.environ.get(
|
||||||
|
"NASA_IMAGES_PATH", "/Volumes/MediaServer/Images/NASA/images"
|
||||||
|
)
|
||||||
|
SNAPSHOT_FILE = os.environ.get("SNAPSHOT_FILE", "./files_snapshot.txt")
|
||||||
|
|
||||||
|
# History tracking
|
||||||
|
image_history = []
|
||||||
|
MAX_HISTORY = 50
|
||||||
|
|
||||||
|
|
||||||
|
def load_snapshot():
|
||||||
|
"""Load the snapshot file to get all available image metadata files"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(SNAPSHOT_FILE):
|
||||||
|
return []
|
||||||
|
|
||||||
|
with open(SNAPSHOT_FILE, "r") as f:
|
||||||
|
lines = [line.strip() for line in f.readlines() if line.strip()]
|
||||||
|
return lines
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading snapshot: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_image_metadata():
|
||||||
|
"""Get a random image from the snapshot"""
|
||||||
|
snapshot_files = load_snapshot()
|
||||||
|
if not snapshot_files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
selected_file = random.choice(snapshot_files)
|
||||||
|
return selected_file
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_data(metadata_filename):
|
||||||
|
"""Extract image data from metadata file"""
|
||||||
|
try:
|
||||||
|
# Construct full path to metadata file
|
||||||
|
metadata_path = os.path.join(METADATA_PATH, metadata_filename)
|
||||||
|
|
||||||
|
if not os.path.exists(metadata_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(metadata_path, "r") as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
# Extract the image name from metadata filename (remove index- prefix and .json suffix)
|
||||||
|
base_name = metadata_filename[6:-5] # Remove "index-" prefix and ".json" suffix
|
||||||
|
|
||||||
|
# Construct full path to image file
|
||||||
|
image_filename = f"{base_name}~orig.jpg"
|
||||||
|
image_path = os.path.join(IMAGES_PATH, image_filename)
|
||||||
|
|
||||||
|
# Parse data structure - it's an array, so get first element
|
||||||
|
description = ""
|
||||||
|
title = base_name
|
||||||
|
|
||||||
|
if (
|
||||||
|
"data" in metadata
|
||||||
|
and isinstance(metadata["data"], list)
|
||||||
|
and len(metadata["data"]) > 0
|
||||||
|
):
|
||||||
|
data_item = metadata["data"][0] # Get the first item from the array
|
||||||
|
|
||||||
|
if isinstance(data_item, dict):
|
||||||
|
# Look for description in the first data element
|
||||||
|
if "description" in data_item:
|
||||||
|
description = data_item["description"]
|
||||||
|
elif "abstract" in data_item:
|
||||||
|
description = data_item["abstract"]
|
||||||
|
elif "title" in data_item:
|
||||||
|
title = data_item["title"]
|
||||||
|
|
||||||
|
# Additional fallbacks
|
||||||
|
if not description and isinstance(data_item, dict):
|
||||||
|
for key, value in data_item.items():
|
||||||
|
if (
|
||||||
|
isinstance(value, str)
|
||||||
|
and len(value) > 20
|
||||||
|
and not key.startswith("_")
|
||||||
|
):
|
||||||
|
if len(value) > 100 or (
|
||||||
|
len(value) > 50 and "space" in value.lower()
|
||||||
|
):
|
||||||
|
description = value
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no description from data array, try root level
|
||||||
|
if not description and "description" in metadata:
|
||||||
|
description = metadata["description"]
|
||||||
|
|
||||||
|
# If still no description, try looking for long strings at root level
|
||||||
|
if not description and isinstance(metadata, dict):
|
||||||
|
for key, value in metadata.items():
|
||||||
|
if isinstance(value, str) and len(value) > 100:
|
||||||
|
description = value
|
||||||
|
break
|
||||||
|
|
||||||
|
# Final fallback
|
||||||
|
if not description:
|
||||||
|
description = "No description available"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": base_name,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"image_path": image_path,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing metadata {metadata_filename}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_history(image_data):
|
||||||
|
"""Add image to history with limit"""
|
||||||
|
global image_history
|
||||||
|
|
||||||
|
# Only add if not already in history
|
||||||
|
if not any(item["id"] == image_data["id"] for item in image_history):
|
||||||
|
image_history.append(image_data)
|
||||||
|
|
||||||
|
# Limit history size
|
||||||
|
if len(image_history) > MAX_HISTORY:
|
||||||
|
image_history.pop(0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_image():
|
||||||
|
"""Get the next image in history or a random one if at end"""
|
||||||
|
if not image_history:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return random image instead of going to next in history
|
||||||
|
# This ensures we always generate new images when at the end
|
||||||
|
metadata_filename = get_random_image_metadata()
|
||||||
|
if metadata_filename:
|
||||||
|
image_data = get_image_data(metadata_filename)
|
||||||
|
return image_data
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_previous_image():
|
||||||
|
"""Get previous image from history"""
|
||||||
|
global image_history
|
||||||
|
|
||||||
|
if len(image_history) >= 2:
|
||||||
|
# Return the second-to-last item (previous in history)
|
||||||
|
return image_history[-2]
|
||||||
|
elif len(image_history) == 1:
|
||||||
|
return image_history[0] # Return current if only one in history
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
"""Main page - get initial random image"""
|
||||||
|
metadata_filename = get_random_image_metadata()
|
||||||
|
if metadata_filename:
|
||||||
|
image_data = get_image_data(metadata_filename)
|
||||||
|
if image_data:
|
||||||
|
add_to_history(image_data)
|
||||||
|
return render_template("index.html", facts=[image_data])
|
||||||
|
|
||||||
|
# Fallback to empty page
|
||||||
|
return render_template("index.html", facts=[])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/facts")
|
||||||
|
def get_facts():
|
||||||
|
"""Return current fact - for backwards compatibility"""
|
||||||
|
if image_history:
|
||||||
|
return jsonify([image_history[-1]])
|
||||||
|
return jsonify([])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/fact/<string:fact_id>")
|
||||||
|
def get_fact(fact_id):
|
||||||
|
"""Get a specific fact by ID - this is primarily for demo purposes"""
|
||||||
|
# In our implementation, we don't actually support this since we're generating random images
|
||||||
|
return jsonify({"error": "Not implemented in this version"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/image/<string:fact_id>")
|
||||||
|
def get_image(fact_id):
|
||||||
|
"""Get image by ID"""
|
||||||
|
try:
|
||||||
|
# This is a bit tricky because our structure is different now
|
||||||
|
# We'll find the metadata file with that id and return its image
|
||||||
|
image_filename = f"{fact_id}~orig.jpg"
|
||||||
|
full_image_path = os.path.join(IMAGES_PATH, image_filename)
|
||||||
|
|
||||||
|
if os.path.exists(full_image_path):
|
||||||
|
return send_file(full_image_path)
|
||||||
|
else:
|
||||||
|
return "Image not found", 404
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}", 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/random-image")
|
||||||
|
def get_random_image():
|
||||||
|
"""Get a random image with metadata"""
|
||||||
|
metadata_filename = get_random_image_metadata()
|
||||||
|
if metadata_filename:
|
||||||
|
image_data = get_image_data(metadata_filename)
|
||||||
|
if image_data:
|
||||||
|
add_to_history(image_data)
|
||||||
|
return jsonify(image_data)
|
||||||
|
return jsonify({"error": "No images available"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/next-image")
|
||||||
|
def get_next_image_route():
|
||||||
|
"""Get next image (random)"""
|
||||||
|
next_image = get_next_image()
|
||||||
|
if next_image:
|
||||||
|
add_to_history(next_image)
|
||||||
|
return jsonify(next_image)
|
||||||
|
return jsonify({"error": "No more images"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/previous-image")
|
||||||
|
def get_previous_image_route():
|
||||||
|
"""Get previous image from history"""
|
||||||
|
prev_image = get_previous_image()
|
||||||
|
if prev_image:
|
||||||
|
return jsonify(prev_image)
|
||||||
|
return jsonify({"error": "No previous image"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True, host="0.0.0.0", port=6008)
|
||||||
2
load_snapshot.sh
Normal file
2
load_snapshot.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ls /Volumes/MediaServer/Images/NASA/metadata >> files_snapshot.txt
|
||||||
|
ls /mnt/centralstoragemedia/Images/NASA/metadata >> files_snapshot.txt
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
Werkzeug==2.3.7
|
||||||
127
static/css/style.css
Normal file
127
static/css/style.css
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/* Reset and base styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image container with overlay */
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||||
|
padding: 20px;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
pointer-events: none; /* Allow clicks to pass through to image */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-overlay.hidden {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#factTitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#factDescription {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
gap: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
pointer-events: auto; /* Allow button clicks */
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimization */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#factTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#factDescription {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.controls {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#factTitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#factDescription {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
static/js/script.js
Normal file
114
static/js/script.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Global variables
|
||||||
|
let currentFact = null;
|
||||||
|
let imageHistory = [];
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
const imageContainer = document.getElementById("imageContainer");
|
||||||
|
const spaceImage = document.getElementById("spaceImage");
|
||||||
|
const factOverlay = document.getElementById("factOverlay");
|
||||||
|
const factTitle = document.getElementById("factTitle");
|
||||||
|
const factDescription = document.getElementById("factDescription");
|
||||||
|
const prevBtn = document.getElementById("prevBtn");
|
||||||
|
const nextBtn = document.getElementById("nextBtn");
|
||||||
|
|
||||||
|
// Fetch random image from API
|
||||||
|
async function fetchRandomImage() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/random-image");
|
||||||
|
const imageData = await response.json();
|
||||||
|
|
||||||
|
if (imageData && !response.error) {
|
||||||
|
currentFact = imageData;
|
||||||
|
loadFact(currentFact);
|
||||||
|
// Add to history
|
||||||
|
imageHistory.push(currentFact);
|
||||||
|
|
||||||
|
// Limit history to 50 items
|
||||||
|
if (imageHistory.length > 50) {
|
||||||
|
imageHistory.shift();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch random image");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching random image:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a specific fact
|
||||||
|
function loadFact(fact) {
|
||||||
|
if (!fact) return;
|
||||||
|
|
||||||
|
spaceImage.src = `/api/image/${fact.id}`;
|
||||||
|
factTitle.textContent = fact.title;
|
||||||
|
factDescription.textContent = fact.description;
|
||||||
|
|
||||||
|
// Make sure overlay is visible when loading new image
|
||||||
|
factOverlay.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle fact overlay visibility
|
||||||
|
function toggleFactOverlay() {
|
||||||
|
if (factOverlay) {
|
||||||
|
factOverlay.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load next image
|
||||||
|
async function loadNextImage() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/next-image");
|
||||||
|
const imageData = await response.json();
|
||||||
|
|
||||||
|
if (imageData && !response.error) {
|
||||||
|
currentFact = imageData;
|
||||||
|
loadFact(currentFact);
|
||||||
|
|
||||||
|
// Add to history
|
||||||
|
imageHistory.push(currentFact);
|
||||||
|
|
||||||
|
// Limit history to 50 items
|
||||||
|
if (imageHistory.length > 50) {
|
||||||
|
imageHistory.shift();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch next image");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching next image:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load previous image from history
|
||||||
|
async function loadPreviousImage() {
|
||||||
|
if (imageHistory.length < 2) {
|
||||||
|
// If we're at the beginning, go back to random image
|
||||||
|
await fetchRandomImage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove current item from history and get previous one
|
||||||
|
imageHistory.pop(); // Remove current
|
||||||
|
const prevFact = imageHistory[imageHistory.length - 1];
|
||||||
|
|
||||||
|
if (prevFact) {
|
||||||
|
currentFact = prevFact;
|
||||||
|
loadFact(currentFact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
imageContainer.addEventListener("click", toggleFactOverlay);
|
||||||
|
|
||||||
|
prevBtn.addEventListener("click", () => {
|
||||||
|
loadPreviousImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
nextBtn.addEventListener("click", () => {
|
||||||
|
loadNextImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the app
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
fetchRandomImage();
|
||||||
|
});
|
||||||
26
templates/index.html
Normal file
26
templates/index.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Space Facts Viewer</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for("static", filename="css/style.css") }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="image-container" id="imageContainer">
|
||||||
|
<img id="spaceImage" src="" alt="Space Image" class="space-image">
|
||||||
|
<div class="fact-overlay" id="factOverlay">
|
||||||
|
<h2 id="factTitle"></h2>
|
||||||
|
<p id="factDescription"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="prevBtn">Previous</button>
|
||||||
|
<button id="nextBtn">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for("static", filename="js/script.js") }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user