vumichien commited on
Commit
f7596a7
·
1 Parent(s): af9ec2c
Files changed (6) hide show
  1. Dockerfile +19 -0
  2. app.py +227 -0
  3. requirements.txt +7 -0
  4. templates/index.html +271 -0
  5. templates/login.html +76 -0
  6. templates/view.html +136 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy requirements first to leverage Docker cache
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy application code
10
+ COPY . .
11
+
12
+ # Create upload directory
13
+ RUN mkdir -p static/uploads
14
+
15
+ # Expose port for Hugging Face Spaces (uses port 7860)
16
+ EXPOSE 7860
17
+
18
+ # Run the application
19
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Request, HTTPException, Form, Depends, status
2
+ from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials
6
+ import shutil
7
+ import os
8
+ import uuid
9
+ import base64
10
+ from pathlib import Path
11
+ import uvicorn
12
+ from typing import List, Optional
13
+ import secrets
14
+ from starlette.middleware.sessions import SessionMiddleware
15
+ from fastapi.security import OAuth2PasswordRequestForm
16
+ from fastapi.responses import JSONResponse
17
+
18
+ # Create FastAPI app
19
+ app = FastAPI(title="Image Uploader")
20
+
21
+ # Add session middleware
22
+ app.add_middleware(
23
+ SessionMiddleware,
24
+ secret_key="YOUR_SECRET_KEY_CHANGE_THIS_IN_PRODUCTION"
25
+ )
26
+
27
+ # Create uploads directory if it doesn't exist
28
+ UPLOAD_DIR = Path("static/uploads")
29
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
30
+
31
+ # Mount static directory
32
+ app.mount("/static", StaticFiles(directory="static"), name="static")
33
+
34
+ # Set up Jinja2 templates
35
+ templates = Jinja2Templates(directory="templates")
36
+
37
+ # Set up security
38
+ security = HTTPBasic()
39
+
40
+ # Hardcoded credentials (in a real app, use proper hashed passwords in a database)
41
+ USERNAME = "detomo"
42
+ PASSWORD = "itweek2025"
43
+
44
+ def get_file_extension(filename: str) -> str:
45
+ """Get the file extension from a filename."""
46
+ return os.path.splitext(filename)[1].lower()
47
+
48
+ def is_valid_image(extension: str) -> bool:
49
+ """Check if the file extension is a valid image type."""
50
+ return extension in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
51
+
52
+ def authenticate(request: Request):
53
+ """Check if user is authenticated."""
54
+ is_authenticated = request.session.get("authenticated", False)
55
+ return is_authenticated
56
+
57
+ def verify_auth(request: Request):
58
+ """Verify authentication."""
59
+ if not authenticate(request):
60
+ raise HTTPException(
61
+ status_code=status.HTTP_401_UNAUTHORIZED,
62
+ detail="Not authenticated",
63
+ headers={"WWW-Authenticate": "Basic"},
64
+ )
65
+ return True
66
+
67
+ @app.get("/login", response_class=HTMLResponse)
68
+ async def login_page(request: Request):
69
+ """Render the login page."""
70
+ # If already authenticated, redirect to home
71
+ if authenticate(request):
72
+ return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
73
+
74
+ return templates.TemplateResponse(
75
+ "login.html",
76
+ {"request": request}
77
+ )
78
+
79
+ @app.post("/login")
80
+ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
81
+ """Handle login form submission."""
82
+ if form_data.username == USERNAME and form_data.password == PASSWORD:
83
+ request.session["authenticated"] = True
84
+ return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
85
+ else:
86
+ return templates.TemplateResponse(
87
+ "login.html",
88
+ {"request": request, "error": "Invalid username or password"}
89
+ )
90
+
91
+ @app.get("/logout")
92
+ async def logout(request: Request):
93
+ """Handle logout."""
94
+ request.session.pop("authenticated", None)
95
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
96
+
97
+ @app.get("/", response_class=HTMLResponse)
98
+ async def home(request: Request):
99
+ """Render the home page with authentication check."""
100
+ # Check if user is authenticated
101
+ if not authenticate(request):
102
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
103
+
104
+ # Get all uploaded images
105
+ uploaded_images = []
106
+
107
+ if UPLOAD_DIR.exists():
108
+ for file in UPLOAD_DIR.iterdir():
109
+ if is_valid_image(get_file_extension(file.name)):
110
+ image_url = f"/static/uploads/{file.name}"
111
+ uploaded_images.append({
112
+ "name": file.name,
113
+ "url": image_url,
114
+ "embed_url": f"{request.base_url}static/uploads/{file.name}"
115
+ })
116
+
117
+ return templates.TemplateResponse(
118
+ "index.html",
119
+ {"request": request, "uploaded_images": uploaded_images}
120
+ )
121
+
122
+ @app.post("/upload/")
123
+ async def upload_image(request: Request, file: UploadFile = File(...)):
124
+ """Handle image upload with authentication check."""
125
+ # Check if user is authenticated
126
+ if not authenticate(request):
127
+ return JSONResponse(
128
+ status_code=status.HTTP_401_UNAUTHORIZED,
129
+ content={"detail": "Not authenticated"}
130
+ )
131
+
132
+ # Check if the file is an image
133
+ extension = get_file_extension(file.filename)
134
+ if not is_valid_image(extension):
135
+ raise HTTPException(status_code=400, detail="Only image files are allowed")
136
+
137
+ # Generate a unique filename to prevent overwrites
138
+ unique_filename = f"{uuid.uuid4()}{extension}"
139
+ file_path = UPLOAD_DIR / unique_filename
140
+
141
+ # Save the file
142
+ with file_path.open("wb") as buffer:
143
+ shutil.copyfileobj(file.file, buffer)
144
+
145
+ # Return the file URL and embed code
146
+ file_url = f"/static/uploads/{unique_filename}"
147
+
148
+ # For base64 encoding
149
+ file.file.seek(0) # Reset file pointer to beginning
150
+ contents = await file.read()
151
+ base64_encoded = base64.b64encode(contents).decode("utf-8")
152
+
153
+ # Determine MIME type
154
+ mime_type = {
155
+ '.jpg': 'image/jpeg',
156
+ '.jpeg': 'image/jpeg',
157
+ '.png': 'image/png',
158
+ '.gif': 'image/gif',
159
+ '.bmp': 'image/bmp',
160
+ '.webp': 'image/webp'
161
+ }.get(extension, 'application/octet-stream')
162
+
163
+ return {
164
+ "success": True,
165
+ "file_name": unique_filename,
166
+ "file_url": file_url,
167
+ "full_url": f"{request.base_url}static/uploads/{unique_filename}",
168
+ "embed_html": f'<img src="{request.base_url}static/uploads/{unique_filename}" alt="Uploaded Image" />',
169
+ "base64_data": f"data:{mime_type};base64,{base64_encoded[:20]}...{base64_encoded[-20:]}",
170
+ "base64_embed": f'<img src="data:{mime_type};base64,{base64_encoded}" alt="Embedded Image" />'
171
+ }
172
+
173
+ @app.get("/view/{file_name}")
174
+ async def view_image(request: Request, file_name: str):
175
+ """View a specific image with authentication check."""
176
+ # Check if user is authenticated
177
+ if not authenticate(request):
178
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
179
+
180
+ file_path = UPLOAD_DIR / file_name
181
+
182
+ if not file_path.exists():
183
+ raise HTTPException(status_code=404, detail="Image not found")
184
+
185
+ image_url = f"/static/uploads/{file_name}"
186
+ embed_url = f"{request.base_url}static/uploads/{file_name}"
187
+
188
+ return templates.TemplateResponse(
189
+ "view.html",
190
+ {
191
+ "request": request,
192
+ "image_url": image_url,
193
+ "file_name": file_name,
194
+ "embed_url": embed_url
195
+ }
196
+ )
197
+
198
+ @app.delete("/delete/{file_name}")
199
+ async def delete_image(request: Request, file_name: str):
200
+ """Delete an image with authentication check."""
201
+ # Check if user is authenticated
202
+ if not authenticate(request):
203
+ return JSONResponse(
204
+ status_code=status.HTTP_401_UNAUTHORIZED,
205
+ content={"detail": "Not authenticated"}
206
+ )
207
+
208
+ file_path = UPLOAD_DIR / file_name
209
+
210
+ if not file_path.exists():
211
+ raise HTTPException(status_code=404, detail="Image not found")
212
+
213
+ os.remove(file_path)
214
+
215
+ return {"success": True, "message": f"Image {file_name} has been deleted"}
216
+
217
+ # Health check endpoint for Hugging Face Spaces
218
+ @app.get("/healthz")
219
+ async def health_check():
220
+ return {"status": "ok"}
221
+
222
+ if __name__ == "__main__":
223
+ # For local development
224
+ uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)
225
+
226
+ # For production/Hugging Face (uncomment when deploying)
227
+ # uvicorn.run("app:app", host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ jinja2
5
+ aiofiles
6
+ itsdangerous
7
+ python-jose
templates/index.html ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- templates/index.html -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Image Uploader</title>
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <style>
10
+ .upload-container {
11
+ background-color: #f8f9fa;
12
+ padding: 20px;
13
+ border-radius: 8px;
14
+ margin-bottom: 30px;
15
+ }
16
+ .image-card {
17
+ margin-bottom: 20px;
18
+ transition: transform 0.3s;
19
+ }
20
+ .image-card:hover {
21
+ transform: translateY(-5px);
22
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
23
+ }
24
+ .copy-btn {
25
+ cursor: pointer;
26
+ }
27
+ .image-preview {
28
+ max-height: 200px;
29
+ object-fit: cover;
30
+ width: 100%;
31
+ }
32
+ .code-container {
33
+ background-color: #f5f5f5;
34
+ padding: 10px;
35
+ border-radius: 4px;
36
+ margin-top: 10px;
37
+ font-family: monospace;
38
+ font-size: 0.8rem;
39
+ overflow-x: auto;
40
+ white-space: nowrap;
41
+ }
42
+ .hidden {
43
+ display: none;
44
+ }
45
+ #uploadProgress {
46
+ margin-top: 10px;
47
+ }
48
+ .success-message {
49
+ background-color: #d1e7dd;
50
+ color: #0f5132;
51
+ padding: 15px;
52
+ border-radius: 4px;
53
+ margin-top: 15px;
54
+ }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div class="container py-5">
59
+ <div class="d-flex justify-content-between align-items-center mb-4">
60
+ <h1>🖼️ Image Uploader & Embed Link Generator</h1>
61
+ <a href="/logout" class="btn btn-outline-danger">Logout</a>
62
+ </div>
63
+
64
+ <div class="upload-container">
65
+ <h2>Upload New Image</h2>
66
+ <form id="uploadForm" enctype="multipart/form-data">
67
+ <div class="mb-3">
68
+ <label for="file" class="form-label">Select an image to upload</label>
69
+ <input class="form-control" type="file" id="file" name="file" accept="image/*">
70
+ </div>
71
+ <button type="submit" class="btn btn-primary">Upload & Generate Links</button>
72
+ </form>
73
+
74
+ <div id="uploadProgress" class="progress hidden">
75
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
76
+ </div>
77
+
78
+ <div id="uploadResult" class="hidden success-message">
79
+ <h4>✅ Upload Successful!</h4>
80
+ <div id="previewContainer" class="mt-3">
81
+ <img id="imagePreview" class="img-fluid img-thumbnail mb-3" alt="Uploaded image preview">
82
+ </div>
83
+
84
+ <h5>Embed Options:</h5>
85
+
86
+ <div class="mb-3">
87
+ <h6>1. Direct URL (hosted on this server)</h6>
88
+ <div class="input-group">
89
+ <input type="text" id="directUrl" class="form-control" readonly>
90
+ <button class="btn btn-outline-secondary copy-btn" data-target="directUrl">Copy</button>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="mb-3">
95
+ <h6>2. HTML Embed Code</h6>
96
+ <div class="code-container" id="htmlEmbed"></div>
97
+ <button class="btn btn-sm btn-outline-secondary mt-2 copy-btn" data-target="htmlEmbed">Copy HTML</button>
98
+ </div>
99
+
100
+ <div class="mb-3">
101
+ <h6>3. Base64 Embed (works anywhere without hosting)</h6>
102
+ <div class="code-container" id="base64Embed">Base64 code is truncated for display. Use the copy button to get the full code.</div>
103
+ <button class="btn btn-sm btn-outline-secondary mt-2 copy-btn" data-target="base64Embed">Copy Base64 HTML</button>
104
+ </div>
105
+
106
+ <p class="text-muted mt-3">
107
+ <small>Tip: Base64 encoding embeds the image directly in your HTML, so you don't need to host the image separately.</small>
108
+ </p>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="row">
113
+ <div class="col-12">
114
+ <h2 class="mb-4">Your Uploaded Images</h2>
115
+
116
+ {% if not uploaded_images %}
117
+ <div class="alert alert-info">
118
+ No images have been uploaded yet. Upload your first image above!
119
+ </div>
120
+ {% else %}
121
+ <div class="row">
122
+ {% for image in uploaded_images %}
123
+ <div class="col-md-4">
124
+ <div class="card image-card">
125
+ <img src="{{ image.url }}" class="card-img-top image-preview" alt="{{ image.name }}">
126
+ <div class="card-body">
127
+ <h5 class="card-title text-truncate">{{ image.name }}</h5>
128
+ <div class="d-flex justify-content-between mt-3">
129
+ <a href="/view/{{ image.name }}" class="btn btn-sm btn-primary">View Details</a>
130
+ <button class="btn btn-sm btn-danger delete-btn" data-filename="{{ image.name }}">Delete</button>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ {% endfor %}
136
+ </div>
137
+ {% endif %}
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <script>
143
+ document.addEventListener('DOMContentLoaded', function() {
144
+ const uploadForm = document.getElementById('uploadForm');
145
+ const uploadProgress = document.getElementById('uploadProgress');
146
+ const progressBar = uploadProgress.querySelector('.progress-bar');
147
+ const uploadResult = document.getElementById('uploadResult');
148
+
149
+ // Handle form submission
150
+ uploadForm.addEventListener('submit', function(e) {
151
+ e.preventDefault();
152
+
153
+ const fileInput = document.getElementById('file');
154
+ if (!fileInput.files.length) {
155
+ alert('Please select a file to upload.');
156
+ return;
157
+ }
158
+
159
+ const file = fileInput.files[0];
160
+ const formData = new FormData();
161
+ formData.append('file', file);
162
+
163
+ // Show progress
164
+ uploadProgress.classList.remove('hidden');
165
+ progressBar.style.width = '0%';
166
+
167
+ const xhr = new XMLHttpRequest();
168
+
169
+ xhr.upload.addEventListener('progress', function(e) {
170
+ if (e.lengthComputable) {
171
+ const percentComplete = (e.loaded / e.total) * 100;
172
+ progressBar.style.width = percentComplete + '%';
173
+ }
174
+ });
175
+
176
+ xhr.addEventListener('load', function() {
177
+ if (xhr.status === 200) {
178
+ const response = JSON.parse(xhr.responseText);
179
+
180
+ // Update preview
181
+ document.getElementById('imagePreview').src = response.file_url;
182
+ document.getElementById('directUrl').value = window.location.origin + response.file_url;
183
+ document.getElementById('htmlEmbed').textContent = response.embed_html;
184
+
185
+ // Store the full base64 embed, but display truncated version
186
+ const base64EmbedEl = document.getElementById('base64Embed');
187
+ base64EmbedEl.textContent = 'Base64 embed code (truncated for display): ' +
188
+ response.base64_embed.substring(0, 50) + '...' +
189
+ response.base64_embed.substring(response.base64_embed.length - 20);
190
+ base64EmbedEl.dataset.fullCode = response.base64_embed;
191
+
192
+ // Show results
193
+ uploadResult.classList.remove('hidden');
194
+
195
+ // Reset form for next upload
196
+ uploadForm.reset();
197
+
198
+ // Refresh the page after 2 seconds to show the new image in the gallery
199
+ setTimeout(() => {
200
+ window.location.reload();
201
+ }, 2000);
202
+ } else {
203
+ alert('Upload failed. Please try again.');
204
+ }
205
+
206
+ uploadProgress.classList.add('hidden');
207
+ });
208
+
209
+ xhr.addEventListener('error', function() {
210
+ alert('Upload failed. Please try again.');
211
+ uploadProgress.classList.add('hidden');
212
+ });
213
+
214
+ xhr.open('POST', '/upload/');
215
+ xhr.send(formData);
216
+ });
217
+
218
+ // Handle copy buttons
219
+ document.querySelectorAll('.copy-btn').forEach(function(btn) {
220
+ btn.addEventListener('click', function() {
221
+ const targetId = this.dataset.target;
222
+ const targetEl = document.getElementById(targetId);
223
+
224
+ let textToCopy;
225
+ if (targetEl.tagName === 'INPUT') {
226
+ textToCopy = targetEl.value;
227
+ } else {
228
+ // For base64, we have the full code in the data attribute
229
+ textToCopy = targetEl.dataset.fullCode || targetEl.textContent;
230
+ }
231
+
232
+ navigator.clipboard.writeText(textToCopy).then(() => {
233
+ // Change button text temporarily
234
+ const originalText = this.textContent;
235
+ this.textContent = 'Copied!';
236
+ setTimeout(() => {
237
+ this.textContent = originalText;
238
+ }, 1500);
239
+ }).catch(err => {
240
+ console.error('Failed to copy text: ', err);
241
+ });
242
+ });
243
+ });
244
+
245
+ // Handle delete buttons
246
+ document.querySelectorAll('.delete-btn').forEach(function(btn) {
247
+ btn.addEventListener('click', function() {
248
+ const filename = this.dataset.filename;
249
+ if (confirm(`Are you sure you want to delete ${filename}?`)) {
250
+ fetch(`/delete/${filename}`, {
251
+ method: 'DELETE'
252
+ })
253
+ .then(response => response.json())
254
+ .then(data => {
255
+ if (data.success) {
256
+ window.location.reload();
257
+ } else {
258
+ alert('Failed to delete the image.');
259
+ }
260
+ })
261
+ .catch(error => {
262
+ console.error('Error:', error);
263
+ alert('An error occurred while deleting the image.');
264
+ });
265
+ }
266
+ });
267
+ });
268
+ });
269
+ </script>
270
+ </body>
271
+ </html>
templates/login.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- templates/login.html -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Login - Image Uploader</title>
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <style>
10
+ body {
11
+ background-color: #f5f5f5;
12
+ height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ }
17
+ .login-container {
18
+ max-width: 400px;
19
+ padding: 40px;
20
+ background: white;
21
+ border-radius: 10px;
22
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
23
+ }
24
+ .login-title {
25
+ text-align: center;
26
+ margin-bottom: 30px;
27
+ color: #1565C0;
28
+ }
29
+ .login-form {
30
+ margin-bottom: 20px;
31
+ }
32
+ .error-message {
33
+ background-color: #f8d7da;
34
+ color: #721c24;
35
+ padding: 10px;
36
+ border-radius: 4px;
37
+ margin-bottom: 20px;
38
+ }
39
+ .login-footer {
40
+ text-align: center;
41
+ margin-top: 20px;
42
+ color: #6c757d;
43
+ font-size: 0.9rem;
44
+ }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="login-container">
49
+ <h2 class="login-title">🖼️ Image Uploader</h2>
50
+
51
+ {% if error %}
52
+ <div class="error-message">
53
+ {{ error }}
54
+ </div>
55
+ {% endif %}
56
+
57
+ <form class="login-form" method="post" action="/login">
58
+ <div class="mb-3">
59
+ <label for="username" class="form-label">Username</label>
60
+ <input type="text" class="form-control" id="username" name="username" required>
61
+ </div>
62
+ <div class="mb-3">
63
+ <label for="password" class="form-label">Password</label>
64
+ <input type="password" class="form-control" id="password" name="password" required>
65
+ </div>
66
+ <div class="d-grid">
67
+ <button type="submit" class="btn btn-primary">Login</button>
68
+ </div>
69
+ </form>
70
+
71
+ <div class="login-footer">
72
+ Enter your credentials to access the Image Uploader
73
+ </div>
74
+ </div>
75
+ </body>
76
+ </html>
templates/view.html ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>View Image: {{ file_name }}</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ .image-container {
10
+ background-color: #f8f9fa;
11
+ padding: 20px;
12
+ border-radius: 8px;
13
+ margin-bottom: 30px;
14
+ }
15
+ .embed-options {
16
+ background-color: #fff;
17
+ padding: 20px;
18
+ border-radius: 8px;
19
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
20
+ }
21
+ .code-container {
22
+ background-color: #f5f5f5;
23
+ padding: 10px;
24
+ border-radius: 4px;
25
+ margin-top: 10px;
26
+ font-family: monospace;
27
+ font-size: 0.8rem;
28
+ overflow-x: auto;
29
+ white-space: nowrap;
30
+ }
31
+ .copy-btn {
32
+ cursor: pointer;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div class="container py-5">
38
+ <div class="d-flex justify-content-between align-items-center mb-4">
39
+ <h1>Image Details: {{ file_name }}</h1>
40
+ <a href="/" class="btn btn-outline-primary">Back to Gallery</a>
41
+ </div>
42
+
43
+ <div class="row">
44
+ <div class="col-md-8">
45
+ <div class="image-container">
46
+ <img src="{{ image_url }}" class="img-fluid" alt="{{ file_name }}">
47
+ </div>
48
+ </div>
49
+
50
+ <div class="col-md-4">
51
+ <div class="embed-options">
52
+ <h3>Embed Options</h3>
53
+
54
+ <div class="mb-3">
55
+ <h5>1. Direct URL</h5>
56
+ <div class="input-group">
57
+ <input type="text" id="directUrl" class="form-control" value="{{ embed_url }}" readonly>
58
+ <button class="btn btn-outline-secondary copy-btn" data-target="directUrl">Copy</button>
59
+ </div>
60
+ <small class="text-muted">Use this URL to link directly to the image.</small>
61
+ </div>
62
+
63
+ <div class="mb-3">
64
+ <h5>2. HTML Embed Code</h5>
65
+ <div class="code-container" id="htmlEmbed">&lt;img src="{{ embed_url }}" alt="{{ file_name }}" /&gt;</div>
66
+ <button class="btn btn-sm btn-outline-secondary mt-2 copy-btn" data-target="htmlEmbed">Copy HTML</button>
67
+ <small class="text-muted d-block mt-2">Use this code to embed the image in an HTML page.</small>
68
+ </div>
69
+
70
+ <div class="mb-3">
71
+ <h5>3. Markdown Embed</h5>
72
+ <div class="code-container" id="markdownEmbed">![{{ file_name }}]({{ embed_url }})</div>
73
+ <button class="btn btn-sm btn-outline-secondary mt-2 copy-btn" data-target="markdownEmbed">Copy Markdown</button>
74
+ </div>
75
+
76
+ <div class="mt-4">
77
+ <button class="btn btn-danger delete-btn" data-filename="{{ file_name }}">Delete Image</button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <script>
85
+ document.addEventListener('DOMContentLoaded', function() {
86
+ // Handle copy buttons
87
+ document.querySelectorAll('.copy-btn').forEach(function(btn) {
88
+ btn.addEventListener('click', function() {
89
+ const targetId = this.dataset.target;
90
+ const targetEl = document.getElementById(targetId);
91
+
92
+ let textToCopy;
93
+ if (targetEl.tagName === 'INPUT') {
94
+ textToCopy = targetEl.value;
95
+ } else {
96
+ textToCopy = targetEl.textContent;
97
+ }
98
+
99
+ navigator.clipboard.writeText(textToCopy).then(() => {
100
+ // Change button text temporarily
101
+ const originalText = this.textContent;
102
+ this.textContent = 'Copied!';
103
+ setTimeout(() => {
104
+ this.textContent = originalText;
105
+ }, 1500);
106
+ }).catch(err => {
107
+ console.error('Failed to copy text: ', err);
108
+ });
109
+ });
110
+ });
111
+
112
+ // Handle delete button
113
+ document.querySelector('.delete-btn').addEventListener('click', function() {
114
+ const filename = this.dataset.filename;
115
+ if (confirm(`Are you sure you want to delete ${filename}?`)) {
116
+ fetch(`/delete/${filename}`, {
117
+ method: 'DELETE'
118
+ })
119
+ .then(response => response.json())
120
+ .then(data => {
121
+ if (data.success) {
122
+ window.location.href = '/';
123
+ } else {
124
+ alert('Failed to delete the image.');
125
+ }
126
+ })
127
+ .catch(error => {
128
+ console.error('Error:', error);
129
+ alert('An error occurred while deleting the image.');
130
+ });
131
+ }
132
+ });
133
+ });
134
+ </script>
135
+ </body>
136
+ </html>