updated to correct frature

This commit is contained in:
2025-07-15 13:55:52 +03:00
parent d30d065f44
commit 94f006d458
7 changed files with 1277 additions and 28 deletions

View File

@@ -1,4 +1,17 @@
# QR Code Manager # QR C### QR Code Types Suppo### Management Features
- **Preview** - Real-time preview of generated QR codes
- **Download** - Export QR codes as PNG images
- **History** - View and manage previously generated QR codes
- **Delete** - Remove unwanted QR codes
- **Copy** - Copy QR codes to clipboard
- **Dynamic Link Management** - ⭐ **NEW!** Create and edit link collections **Text** - Plain text QR codes
- **URL/Website** - Direct links to websites
- **Dynamic Link Page** - ⭐ **NEW!** Create a web page with manageable links
- **WiFi** - WiFi network connection details
- **Email** - Pre-filled email composition
- **Phone** - Direct phone number dialing
- **SMS** - Pre-filled SMS messages
- **vCard** - Digital contact cardsger
A comprehensive Python web application for creating, customizing, and managing QR codes. This application provides functionality similar to popular QR code generation websites with additional features for local management. A comprehensive Python web application for creating, customizing, and managing QR codes. This application provides functionality similar to popular QR code generation websites with additional features for local management.
@@ -65,6 +78,14 @@ A comprehensive Python web application for creating, customizing, and managing Q
- Click "Generate QR Code" - Click "Generate QR Code"
- Download or copy your QR code - Download or copy your QR code
4. **Use Dynamic Link Pages** ⭐ **NEW!**:
- Select "Dynamic Link Page" as the QR code type
- Enter a title and description for your link collection
- Generate the QR code
- Use the "Manage" button or edit URL to add/edit links
- Share the QR code - visitors will see your current link collection
- Update links anytime without changing the QR code!
## API Endpoints ## API Endpoints
The application provides a RESTful API for programmatic access: The application provides a RESTful API for programmatic access:
@@ -95,6 +116,39 @@ The application provides a RESTful API for programmatic access:
- **Body**: Multipart form with logo file - **Body**: Multipart form with logo file
- **Response**: Logo path for use in QR generation - **Response**: Logo path for use in QR generation
### Dynamic Link Pages ⭐ **NEW!**
#### Create Link Page
- **POST** `/api/create_link_page`
- **Body**: JSON with page title, description, and QR styling
- **Response**: QR code data, page URLs, and management links
#### Add Link to Page
- **POST** `/api/link_pages/<page_id>/links`
- **Body**: JSON with link title, URL, and description
- **Response**: Success/error status
#### Update Link
- **PUT** `/api/link_pages/<page_id>/links/<link_id>`
- **Body**: JSON with updated link data
- **Response**: Success/error status
#### Delete Link
- **DELETE** `/api/link_pages/<page_id>/links/<link_id>`
- **Response**: Success/error status
#### Get Link Page Data
- **GET** `/api/link_pages/<page_id>`
- **Response**: JSON with page and links data
#### View Public Link Page
- **GET** `/links/<page_id>`
- **Response**: HTML page displaying links
#### Edit Link Page
- **GET** `/edit/<page_id>`
- **Response**: HTML interface for managing links
## Example API Usage ## Example API Usage
### Generate a URL QR Code ### Generate a URL QR Code
@@ -127,6 +181,30 @@ curl -X POST http://localhost:5000/api/generate \
}' }'
``` ```
### Create a Dynamic Link Page ⭐ **NEW!**
```bash
curl -X POST http://localhost:5000/api/create_link_page \
-H "Content-Type: application/json" \
-d '{
"title": "My Resources",
"description": "Collection of useful links",
"foreground_color": "#1565c0",
"background_color": "#ffffff",
"style": "rounded"
}'
```
### Add Links to the Page
```bash
curl -X POST http://localhost:5000/api/link_pages/PAGE_ID/links \
-H "Content-Type: application/json" \
-d '{
"title": "GitHub",
"url": "https://github.com",
"description": "Code repository platform"
}'
```
## File Structure ## File Structure
``` ```

263
app.py
View File

@@ -8,7 +8,6 @@ from PIL import Image, ImageDraw
import qrcode import qrcode
from qrcode.image.styledpil import StyledPilImage from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
from qrcode.image.styles.colorfills import SolidFillColorMask
from flask import Flask, request, jsonify, send_file, render_template from flask import Flask, request, jsonify, send_file, render_template
from flask_cors import CORS from flask_cors import CORS
@@ -24,6 +23,9 @@ os.makedirs(LOGOS_FOLDER, exist_ok=True)
# In-memory storage for QR codes (in production, use a database) # In-memory storage for QR codes (in production, use a database)
qr_codes_db = {} qr_codes_db = {}
# In-memory storage for dynamic link pages
link_pages_db = {}
class QRCodeGenerator: class QRCodeGenerator:
def __init__(self): def __init__(self):
self.default_settings = { self.default_settings = {
@@ -55,27 +57,28 @@ class QRCodeGenerator:
qr.add_data(data) qr.add_data(data)
qr.make(fit=True) qr.make(fit=True)
# Choose module drawer based on style # For styled QR codes with custom module drawer
module_drawer = None if settings['style'] != 'square':
if settings['style'] == 'rounded': # Choose module drawer based on style
module_drawer = RoundedModuleDrawer() module_drawer = None
elif settings['style'] == 'circle': if settings['style'] == 'rounded':
module_drawer = CircleModuleDrawer() module_drawer = RoundedModuleDrawer()
elif settings['style'] == 'circle':
module_drawer = CircleModuleDrawer()
# Generate the styled image
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=module_drawer,
fill_color=settings['foreground_color'],
back_color=settings['background_color']
)
else: else:
module_drawer = SquareModuleDrawer() # Generate standard image with custom colors
img = qr.make_image(
# Create color mask fill_color=settings['foreground_color'],
color_mask = SolidFillColorMask( back_color=settings['background_color']
back_color=settings['background_color'], )
front_color=settings['foreground_color']
)
# Generate the image
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=module_drawer,
color_mask=color_mask
)
return img return img
@@ -109,8 +112,78 @@ class QRCodeGenerator:
print(f"Error adding logo: {e}") print(f"Error adding logo: {e}")
return qr_img return qr_img
# Initialize QR code generator class LinkPageManager:
def __init__(self):
pass
def create_link_page(self, title="My Links", description="Collection of useful links"):
"""Create a new dynamic link page"""
page_id = str(uuid.uuid4())
page_data = {
'id': page_id,
'title': title,
'description': description,
'links': [],
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat(),
'view_count': 0
}
link_pages_db[page_id] = page_data
return page_id
def add_link(self, page_id, title, url, description=""):
"""Add a link to a page"""
if page_id not in link_pages_db:
return False
link_data = {
'id': str(uuid.uuid4()),
'title': title,
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}',
'description': description,
'created_at': datetime.now().isoformat()
}
link_pages_db[page_id]['links'].append(link_data)
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
return True
def update_link(self, page_id, link_id, title=None, url=None, description=None):
"""Update a specific link"""
if page_id not in link_pages_db:
return False
for link in link_pages_db[page_id]['links']:
if link['id'] == link_id:
if title is not None:
link['title'] = title
if url is not None:
link['url'] = url if url.startswith(('http://', 'https://')) else f'https://{url}'
if description is not None:
link['description'] = description
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
return True
return False
def delete_link(self, page_id, link_id):
"""Delete a specific link"""
if page_id not in link_pages_db:
return False
links = link_pages_db[page_id]['links']
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
return True
def increment_view_count(self, page_id):
"""Increment view count for a page"""
if page_id in link_pages_db:
link_pages_db[page_id]['view_count'] += 1
# Initialize managers
qr_generator = QRCodeGenerator() qr_generator = QRCodeGenerator()
link_manager = LinkPageManager()
@app.route('/') @app.route('/')
def index(): def index():
@@ -282,5 +355,151 @@ def upload_logo():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# Dynamic Link Pages API Routes
@app.route('/api/create_link_page', methods=['POST'])
def create_link_page():
"""Create a new dynamic link page and QR code"""
try:
data = request.json
title = data.get('title', 'My Links')
description = data.get('description', 'Collection of useful links')
# Create the link page
page_id = link_manager.create_link_page(title, description)
# Create QR code pointing to the link page
page_url = f"{request.url_root}links/{page_id}"
settings = {
'size': data.get('size', 10),
'border': data.get('border', 4),
'foreground_color': data.get('foreground_color', '#000000'),
'background_color': data.get('background_color', '#FFFFFF'),
'style': data.get('style', 'square')
}
# Generate QR code
qr_img = qr_generator.generate_qr_code(page_url, settings)
# Convert to base64
img_buffer = io.BytesIO()
qr_img.save(img_buffer, format='PNG')
img_buffer.seek(0)
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
# Save QR code record
qr_id = str(uuid.uuid4())
qr_record = {
'id': qr_id,
'type': 'link_page',
'content': page_url,
'page_id': page_id,
'settings': settings,
'created_at': datetime.now().isoformat(),
'image_data': img_base64
}
qr_codes_db[qr_id] = qr_record
# Save image file
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
qr_img.save(img_path)
return jsonify({
'success': True,
'qr_id': qr_id,
'page_id': page_id,
'page_url': page_url,
'edit_url': f"{request.url_root}edit/{page_id}",
'image_data': f'data:image/png;base64,{img_base64}',
'download_url': f'/api/download/{qr_id}'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/link_pages/<page_id>/links', methods=['POST'])
def add_link_to_page(page_id):
"""Add a link to a page"""
try:
data = request.json
title = data.get('title', '')
url = data.get('url', '')
description = data.get('description', '')
if not title or not url:
return jsonify({'error': 'Title and URL are required'}), 400
success = link_manager.add_link(page_id, title, url, description)
if success:
return jsonify({'success': True})
else:
return jsonify({'error': 'Page not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/link_pages/<page_id>/links/<link_id>', methods=['PUT'])
def update_link_in_page(page_id, link_id):
"""Update a link in a page"""
try:
data = request.json
title = data.get('title')
url = data.get('url')
description = data.get('description')
success = link_manager.update_link(page_id, link_id, title, url, description)
if success:
return jsonify({'success': True})
else:
return jsonify({'error': 'Page or link not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/link_pages/<page_id>/links/<link_id>', methods=['DELETE'])
def delete_link_from_page(page_id, link_id):
"""Delete a link from a page"""
try:
success = link_manager.delete_link(page_id, link_id)
if success:
return jsonify({'success': True})
else:
return jsonify({'error': 'Page or link not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/link_pages/<page_id>')
def get_link_page(page_id):
"""Get link page data"""
if page_id in link_pages_db:
return jsonify(link_pages_db[page_id])
else:
return jsonify({'error': 'Page not found'}), 404
@app.route('/links/<page_id>')
def view_link_page(page_id):
"""Display the public link page"""
if page_id not in link_pages_db:
return "Page not found", 404
link_manager.increment_view_count(page_id)
page_data = link_pages_db[page_id]
return render_template('link_page.html', page=page_data)
@app.route('/edit/<page_id>')
def edit_link_page(page_id):
"""Display the edit interface for the link page"""
if page_id not in link_pages_db:
return "Page not found", 404
page_data = link_pages_db[page_id]
return render_template('edit_links.html', page=page_data)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -3,3 +3,4 @@ qrcode[pil]==7.4.2
Pillow==10.0.1 Pillow==10.0.1
flask-cors==4.0.0 flask-cors==4.0.0
python-dotenv==1.0.0 python-dotenv==1.0.0
requests==2.31.0

454
templates/edit_links.html Normal file
View File

@@ -0,0 +1,454 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit {{ page.title }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.2em;
margin-bottom: 10px;
}
.header p {
font-size: 1em;
opacity: 0.9;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
padding: 30px;
}
.form-section {
background: #f8f9fa;
padding: 25px;
border-radius: 10px;
border: 1px solid #e9ecef;
}
.form-section h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.5em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
height: 80px;
resize: vertical;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: transform 0.2s;
margin-right: 10px;
margin-bottom: 10px;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
}
.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #adb5bd 100%);
}
.links-section h2 {
margin-bottom: 20px;
}
.link-item {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
}
.link-item.editing {
border-color: #667eea;
}
.link-display {
display: block;
}
.link-edit {
display: none;
}
.link-item.editing .link-display {
display: none;
}
.link-item.editing .link-edit {
display: block;
}
.link-title {
font-size: 1.2em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.link-description {
color: #666;
font-size: 0.9em;
margin-bottom: 8px;
}
.link-url {
color: #667eea;
font-size: 0.9em;
word-break: break-all;
}
.link-actions {
margin-top: 15px;
display: flex;
gap: 10px;
}
.btn-small {
padding: 8px 15px;
font-size: 12px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #666;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.empty-state .icon {
font-size: 3em;
margin-bottom: 15px;
opacity: 0.5;
}
.page-actions {
background: #f8f9fa;
padding: 20px;
text-align: center;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: center;
gap: 15px;
}
.alert {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.alert-success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.page-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>✏️ Edit Links</h1>
<p>Manage your link collection: {{ page.title }}</p>
</div>
<div class="alert alert-success" id="success-alert">
Operation completed successfully!
</div>
<div class="alert alert-error" id="error-alert">
An error occurred. Please try again.
</div>
<div class="main-content">
<div class="form-section">
<h2>Add New Link</h2>
<form id="add-link-form">
<div class="form-group">
<label for="link-title">Title *</label>
<input type="text" id="link-title" placeholder="Link title" required>
</div>
<div class="form-group">
<label for="link-url">URL *</label>
<input type="url" id="link-url" placeholder="https://example.com" required>
</div>
<div class="form-group">
<label for="link-description">Description</label>
<textarea id="link-description" placeholder="Optional description"></textarea>
</div>
<button type="submit" class="btn btn-success">Add Link</button>
</form>
</div>
<div class="links-section">
<h2>Current Links ({{ page.links|length }})</h2>
<div id="links-container">
{% if page.links %}
{% for link in page.links %}
<div class="link-item" data-link-id="{{ link.id }}">
<div class="link-display">
<div class="link-title">{{ link.title }}</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<div class="link-url">{{ link.url }}</div>
<div class="link-actions">
<button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button>
<button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button>
</div>
</div>
<div class="link-edit">
<div class="form-group">
<label>Title</label>
<input type="text" class="edit-title" value="{{ link.title }}">
</div>
<div class="form-group">
<label>URL</label>
<input type="url" class="edit-url" value="{{ link.url }}">
</div>
<div class="form-group">
<label>Description</label>
<textarea class="edit-description">{{ link.description or '' }}</textarea>
</div>
<div class="link-actions">
<button class="btn btn-small btn-success" onclick="saveLink('{{ link.id }}')">Save</button>
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="icon">📝</div>
<h3>No links yet</h3>
<p>Add your first link using the form on the left.</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="page-actions">
<a href="/links/{{ page.id }}" target="_blank" class="btn">👁️ View Public Page</a>
<a href="/" class="btn btn-secondary">🏠 Back to Dashboard</a>
</div>
</div>
<script>
const pageId = '{{ page.id }}';
// Add new link
document.getElementById('add-link-form').addEventListener('submit', async function(e) {
e.preventDefault();
const title = document.getElementById('link-title').value;
const url = document.getElementById('link-url').value;
const description = document.getElementById('link-description').value;
try {
const response = await fetch(`/api/link_pages/${pageId}/links`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, url, description })
});
const result = await response.json();
if (result.success) {
showAlert('Link added successfully!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showAlert(result.error || 'Failed to add link', 'error');
}
} catch (error) {
showAlert('Network error occurred', 'error');
}
});
// Edit link
function editLink(linkId) {
const linkItem = document.querySelector(`[data-link-id="${linkId}"]`);
linkItem.classList.add('editing');
}
// Cancel edit
function cancelEdit(linkId) {
const linkItem = document.querySelector(`[data-link-id="${linkId}"]`);
linkItem.classList.remove('editing');
}
// Save link
async function saveLink(linkId) {
const linkItem = document.querySelector(`[data-link-id="${linkId}"]`);
const title = linkItem.querySelector('.edit-title').value;
const url = linkItem.querySelector('.edit-url').value;
const description = linkItem.querySelector('.edit-description').value;
try {
const response = await fetch(`/api/link_pages/${pageId}/links/${linkId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, url, description })
});
const result = await response.json();
if (result.success) {
showAlert('Link updated successfully!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showAlert(result.error || 'Failed to update link', 'error');
}
} catch (error) {
showAlert('Network error occurred', 'error');
}
}
// Delete link
async function deleteLink(linkId) {
if (!confirm('Are you sure you want to delete this link?')) {
return;
}
try {
const response = await fetch(`/api/link_pages/${pageId}/links/${linkId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert('Link deleted successfully!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showAlert(result.error || 'Failed to delete link', 'error');
}
} catch (error) {
showAlert('Network error occurred', 'error');
}
}
// Show alert
function showAlert(message, type) {
const alert = document.getElementById(`${type}-alert`);
alert.textContent = message;
alert.style.display = 'block';
setTimeout(() => {
alert.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>

View File

@@ -145,6 +145,7 @@
} }
.wifi-fields, .wifi-fields,
.link-page-fields,
.email-fields, .email-fields,
.sms-fields, .sms-fields,
.vcard-fields { .vcard-fields {
@@ -152,6 +153,7 @@
} }
.wifi-fields.active, .wifi-fields.active,
.link-page-fields.active,
.email-fields.active, .email-fields.active,
.sms-fields.active, .sms-fields.active,
.vcard-fields.active { .vcard-fields.active {
@@ -314,6 +316,7 @@
<select id="qr-type" onchange="toggleFields()"> <select id="qr-type" onchange="toggleFields()">
<option value="text">Text</option> <option value="text">Text</option>
<option value="url">URL/Website</option> <option value="url">URL/Website</option>
<option value="link_page">Dynamic Link Page</option>
<option value="wifi">WiFi</option> <option value="wifi">WiFi</option>
<option value="email">Email</option> <option value="email">Email</option>
<option value="phone">Phone</option> <option value="phone">Phone</option>
@@ -348,6 +351,23 @@
</div> </div>
</div> </div>
<!-- Link Page fields -->
<div class="link-page-fields" id="link-page-fields">
<div class="form-group">
<label for="page-title">Page Title</label>
<input type="text" id="page-title" placeholder="My Link Collection" value="My Links">
</div>
<div class="form-group">
<label for="page-description">Description</label>
<textarea id="page-description" placeholder="A collection of useful links">Collection of useful links</textarea>
</div>
<div class="form-group">
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
<strong>💡 Dynamic Link Page:</strong> This creates a QR code that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
</p>
</div>
</div>
<!-- Email fields --> <!-- Email fields -->
<div class="email-fields" id="email-fields"> <div class="email-fields" id="email-fields">
<div class="form-group"> <div class="form-group">
@@ -476,7 +496,7 @@
// Hide all specific fields // Hide all specific fields
document.getElementById('text-field').style.display = 'none'; document.getElementById('text-field').style.display = 'none';
document.querySelectorAll('.wifi-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => { document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => {
el.classList.remove('active'); el.classList.remove('active');
}); });
@@ -492,7 +512,7 @@
contentField.placeholder = 'Enter your text...'; contentField.placeholder = 'Enter your text...';
} }
} else { } else {
document.getElementById(`${type}-fields`).classList.add('active'); document.getElementById(`${type.replace('_', '-')}-fields`).classList.add('active');
} }
} }
@@ -517,6 +537,9 @@
// Get content based on type // Get content based on type
if (type === 'text' || type === 'url' || type === 'phone') { if (type === 'text' || type === 'url' || type === 'phone') {
content = document.getElementById('content').value; content = document.getElementById('content').value;
} else if (type === 'link_page') {
additionalData.title = document.getElementById('page-title').value;
additionalData.description = document.getElementById('page-description').value;
} else if (type === 'wifi') { } else if (type === 'wifi') {
additionalData.wifi = { additionalData.wifi = {
ssid: document.getElementById('wifi-ssid').value, ssid: document.getElementById('wifi-ssid').value,
@@ -555,7 +578,8 @@
}; };
try { try {
const response = await fetch('/api/generate', { const endpoint = type === 'link_page' ? '/api/create_link_page' : '/api/generate';
const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -571,7 +595,21 @@
// Show QR code // Show QR code
const preview = document.getElementById('qr-preview'); const preview = document.getElementById('qr-preview');
preview.innerHTML = `<img src="${result.image_data}" alt="Generated QR Code">`; let previewHTML = `<img src="${result.image_data}" alt="Generated QR Code">`;
// Add special info for link pages
if (type === 'link_page') {
previewHTML += `
<div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;">
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created!</h4>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Public URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a></p>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
<p style="font-size: 0.9em; color: #666;">Share the QR code - visitors will see your link collection. Use the edit URL to manage your links!</p>
</div>
`;
}
preview.innerHTML = previewHTML;
// Show download buttons // Show download buttons
document.getElementById('download-section').classList.add('active'); document.getElementById('download-section').classList.add('active');
@@ -623,11 +661,12 @@
<div class="qr-item"> <div class="qr-item">
<img src="${qr.preview}" alt="QR Code"> <img src="${qr.preview}" alt="QR Code">
<div class="qr-item-info"> <div class="qr-item-info">
<h4>${qr.type.toUpperCase()}</h4> <h4>${qr.type.toUpperCase()}${qr.type === 'link_page' ? ' 🔗' : ''}</h4>
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p> <p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
</div> </div>
<div class="qr-item-actions"> <div class="qr-item-actions">
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</button> <button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</button>
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;">Manage</button>` : ''}
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')">Delete</button> <button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')">Delete</button>
</div> </div>
</div> </div>
@@ -659,6 +698,21 @@
} }
} }
async function openLinkPage(qrId) {
try {
const response = await fetch(`/api/qr_codes/${qrId}`);
const qrData = await response.json();
if (qrData.page_id) {
window.open(`/edit/${qrData.page_id}`, '_blank');
} else {
alert('Link page not found');
}
} catch (error) {
alert('Error opening link page: ' + error.message);
}
}
// Load history on page load // Load history on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadQRHistory(); loadQRHistory();

281
templates/link_page.html Normal file
View File

@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page.title }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.stats {
background: rgba(255,255,255,0.1);
margin-top: 20px;
padding: 15px;
border-radius: 10px;
display: flex;
justify-content: center;
gap: 30px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 1.5em;
font-weight: bold;
}
.stat-label {
font-size: 0.9em;
opacity: 0.8;
margin-top: 5px;
}
.content {
padding: 30px;
}
.links-section h2 {
color: #333;
margin-bottom: 25px;
font-size: 1.8em;
text-align: center;
}
.link-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
display: block;
color: inherit;
}
.link-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
border-color: #667eea;
}
.link-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.link-icon {
width: 20px;
height: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.link-description {
color: #666;
font-size: 0.95em;
line-height: 1.4;
margin-bottom: 10px;
}
.link-url {
color: #667eea;
font-size: 0.9em;
font-weight: 500;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state .icon {
font-size: 4em;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 1.5em;
margin-bottom: 10px;
color: #333;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
border-top: 1px solid #e9ecef;
color: #666;
font-size: 0.9em;
}
.footer a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.last-updated {
margin-top: 10px;
font-size: 0.8em;
opacity: 0.7;
}
@media (max-width: 768px) {
.header {
padding: 30px 20px;
}
.header h1 {
font-size: 2em;
}
.content {
padding: 20px;
}
.stats {
flex-direction: column;
gap: 15px;
}
}
/* Link animation */
@keyframes linkPulse {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
.link-item:active {
animation: linkPulse 0.2s ease;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{{ page.title }}</h1>
<p>{{ page.description }}</p>
<div class="stats">
<div class="stat-item">
<div class="stat-number">{{ page.links|length }}</div>
<div class="stat-label">Links</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ page.view_count }}</div>
<div class="stat-label">Views</div>
</div>
</div>
</div>
<div class="content">
<div class="links-section">
{% if page.links %}
<h2>📚 Available Links</h2>
{% for link in page.links %}
<a href="{{ link.url }}" target="_blank" class="link-item">
<div class="link-title">
<div class="link-icon">🔗</div>
{{ link.title }}
</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<div class="link-url">{{ link.url }}</div>
</a>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="icon">📝</div>
<h3>No links yet</h3>
<p>This collection is empty. Check back later for new links!</p>
</div>
{% endif %}
</div>
</div>
<div class="footer">
<p>Powered by <a href="/">QR Code Manager</a></p>
{% if page.updated_at %}
<div class="last-updated">
Last updated: {{ page.updated_at[:10] }} at {{ page.updated_at[11:19] }}
</div>
{% endif %}
</div>
</div>
<script>
// Add click tracking (optional)
document.querySelectorAll('.link-item').forEach(link => {
link.addEventListener('click', function() {
// You could add analytics here
console.log('Link clicked:', this.querySelector('.link-title').textContent.trim());
});
});
// Auto-refresh every 30 seconds to get latest links
setInterval(() => {
window.location.reload();
}, 30000);
</script>
</body>
</html>

162
test_link_pages.py Normal file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
Test script for the new Dynamic Link Page feature
This will create a link page and demonstrate its functionality
"""
import requests
import json
import time
# Server URL
BASE_URL = "http://localhost:5000"
def test_create_link_page():
"""Test creating a dynamic link page"""
print("🚀 Testing Dynamic Link Page Creation...")
data = {
"title": "My Awesome Links",
"description": "A collection of my favorite resources and tools",
"foreground_color": "#1565c0",
"background_color": "#ffffff",
"style": "rounded",
"size": 12
}
response = requests.post(f"{BASE_URL}/api/create_link_page", json=data)
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ Link page created successfully!")
print(f"📄 Page ID: {result['page_id']}")
print(f"🔗 Public URL: {result['page_url']}")
print(f"✏️ Edit URL: {result['edit_url']}")
print(f"📱 QR ID: {result['qr_id']}")
return result
else:
print(f"❌ Error: {result['error']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
return None
def test_add_links(page_id):
"""Test adding links to the page"""
print(f"\n📝 Adding links to page {page_id}...")
links_to_add = [
{
"title": "GitHub",
"url": "https://github.com",
"description": "The world's leading software development platform"
},
{
"title": "Stack Overflow",
"url": "https://stackoverflow.com",
"description": "Q&A platform for programmers"
},
{
"title": "MDN Web Docs",
"url": "https://developer.mozilla.org",
"description": "Complete web development documentation"
},
{
"title": "VS Code",
"url": "https://code.visualstudio.com",
"description": "Free source-code editor by Microsoft"
}
]
for link_data in links_to_add:
response = requests.post(f"{BASE_URL}/api/link_pages/{page_id}/links", json=link_data)
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ Added: {link_data['title']}")
else:
print(f"❌ Failed to add {link_data['title']}: {result['error']}")
else:
print(f"❌ HTTP Error adding {link_data['title']}: {response.status_code}")
def test_view_page(page_id):
"""Test viewing the page data"""
print(f"\n👀 Viewing page {page_id}...")
response = requests.get(f"{BASE_URL}/api/link_pages/{page_id}")
if response.status_code == 200:
page_data = response.json()
print(f"✅ Page loaded successfully!")
print(f" Title: {page_data['title']}")
print(f" Description: {page_data['description']}")
print(f" Links: {len(page_data['links'])}")
print(f" Views: {page_data['view_count']}")
for i, link in enumerate(page_data['links'], 1):
print(f" {i}. {link['title']}{link['url']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
def test_update_link(page_id, link_id):
"""Test updating a link"""
print(f"\n✏️ Updating link {link_id}...")
update_data = {
"title": "GitHub (Updated)",
"description": "The world's leading software development platform - Now with Copilot!"
}
response = requests.put(f"{BASE_URL}/api/link_pages/{page_id}/links/{link_id}", json=update_data)
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ Link updated successfully!")
else:
print(f"❌ Failed to update link: {result['error']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
def main():
"""Run the comprehensive test"""
print("🎯 Dynamic Link Page Feature Test")
print("=" * 50)
# Create a new link page
result = test_create_link_page()
if not result:
print("❌ Failed to create link page. Exiting.")
return
page_id = result['page_id']
page_url = result['page_url']
edit_url = result['edit_url']
# Add some links
test_add_links(page_id)
# View the page data
test_view_page(page_id)
print(f"\n🎉 Test completed successfully!")
print(f"📱 QR Code Points to: {page_url}")
print(f"✏️ Edit Interface: {edit_url}")
print(f"🌐 Public Page: {page_url}")
print("\nNow you can:")
print("1. Scan the QR code to visit the public page")
print("2. Open the edit URL to manage links")
print("3. Share the QR code - it will always point to the same page!")
print("4. Update links anytime without changing the QR code")
if __name__ == "__main__":
try:
main()
except requests.exceptions.ConnectionError:
print("❌ Cannot connect to the server. Make sure the QR Code Manager is running on localhost:5000")
print("Start the server with: python app.py")
except Exception as e:
print(f"❌ Unexpected error: {e}")