mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Add image show random app
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
from django.views.generic import TemplateView
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
class MiniAppsListView(TemplateView):
|
||||
template_name = 'links/mini_apps/list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Define mini apps with their info
|
||||
mini_apps = [
|
||||
{
|
||||
'name': 'Image Gallery',
|
||||
'description': 'A beautiful fullscreen image slideshow application with smooth transitions and auto-play functionality.',
|
||||
'url': 'mini-apps-image-gallery',
|
||||
'thumbnail': 'https://picsum.photos/seed/picsum/400/300?fit=crop',
|
||||
'icon': 'fas fa-images',
|
||||
'color': '#3498db'
|
||||
},
|
||||
{
|
||||
'name': 'Weather Dashboard',
|
||||
'description': 'Real-time weather information with beautiful visualizations and forecasts.',
|
||||
'url': '#',
|
||||
'thumbnail': 'https://images.unsplash.com/photo-1504608524841-42fe6f032b4b?w=400&h=300&fit=crop',
|
||||
'icon': 'fas fa-cloud-sun',
|
||||
'color': '#e74c3c'
|
||||
},
|
||||
{
|
||||
'name': 'Music Player',
|
||||
'description': 'Stream and organize your favorite music with an elegant interface.',
|
||||
'url': '#',
|
||||
'thumbnail': 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=400&h=300&fit=crop',
|
||||
'icon': 'fas fa-music',
|
||||
'color': '#9b59b6'
|
||||
},
|
||||
{
|
||||
'name': 'Task Manager',
|
||||
'description': 'Organize your tasks and boost productivity with smart scheduling.',
|
||||
'url': '#',
|
||||
'thumbnail': 'https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?w=400&h=300&fit=crop',
|
||||
'icon': 'fas fa-tasks',
|
||||
'color': '#2ecc71'
|
||||
},
|
||||
{
|
||||
'name': 'Code Editor',
|
||||
'description': 'A lightweight code editor with syntax highlighting and themes.',
|
||||
'url': '#',
|
||||
'thumbnail': 'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=400&h=300&fit=crop',
|
||||
'icon': 'fas fa-code',
|
||||
'color': '#f39c12'
|
||||
},
|
||||
{
|
||||
'name': 'Calculator',
|
||||
'description': 'Scientific calculator with advanced mathematical functions.',
|
||||
'url': '#',
|
||||
'thumbnail': 'https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=400&h=300&fit=crop',
|
||||
'icon': 'fas fa-calculator',
|
||||
'color': '#1abc9c'
|
||||
}
|
||||
]
|
||||
|
||||
context['mini_apps'] = mini_apps
|
||||
return context
|
||||
|
||||
|
||||
class ImageGalleryView(TemplateView):
|
||||
template_name = 'links/mini_apps/image_gallery.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Add any context data needed for the image gallery
|
||||
return context
|
||||
@@ -0,0 +1,560 @@
|
||||
{% extends 'base_blank.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Image Gallery" %} - {% trans "GoLinks" %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
background-color: #0a0a0a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slideshow-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.slide {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.slide.active {
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Animation effects */
|
||||
.fade {
|
||||
transition: opacity 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-left {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-left.active {
|
||||
transform: translateX(0);
|
||||
transition: transform 1.5s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.slide-right {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-right.active {
|
||||
transform: translateX(0);
|
||||
transition: transform 1.5s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.zoom {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.zoom.active {
|
||||
transform: scale(1);
|
||||
transition: transform 1.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 50px;
|
||||
padding: 15px 25px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 13px;
|
||||
margin-bottom: 5px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select, input {
|
||||
background: rgba(50, 50, 50, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
padding: 8px 15px;
|
||||
border-radius: 25px;
|
||||
min-width: 120px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select:focus, input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
option {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: rgba(50, 50, 50, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
padding: 8px 15px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: rgba(70, 70, 70, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
z-index: 5;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 15px 40px;
|
||||
border-radius: 30px;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.title span {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||
border-top: 4px solid #4ecdc4;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 30px;
|
||||
z-index: 5;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.app-logo i {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
z-index: 5;
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
|
||||
z-index: 15;
|
||||
width: 0%;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
color: #4ecdc4;
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.preview-item:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.preview-item.active {
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 0 15px rgba(255, 107, 107, 0.5);
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 100px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
z-index: 5;
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(50, 50, 50, 0.8);
|
||||
color: #4ecdc4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
border-radius: 20px;
|
||||
bottom: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
top: 20px;
|
||||
padding: 10px 20px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.photo-info {
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
bottom: 150px;
|
||||
}
|
||||
|
||||
|
||||
.back-button {
|
||||
right: 30px;
|
||||
top: 80px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="app-container">
|
||||
<div class="progress-bar"></div>
|
||||
<div class="app-logo">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>RandomImage</span>
|
||||
</div>
|
||||
<a href="{% url 'mini-apps-list' %}" class="back-button">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Mini Apps" %}
|
||||
</a>
|
||||
<div class="slideshow-container" id="slideshow"></div>
|
||||
<div class="control-panel">
|
||||
<div class="control-group">
|
||||
<div class="control-label">{% trans "Animation Effect" %}</div>
|
||||
<select id="animationType">
|
||||
<option value="fade">{% trans "Fade In/Out" %}</option>
|
||||
<option value="slide-left">{% trans "Slide Left" %}</option>
|
||||
<option value="slide-right">{% trans "Slide Right" %}</option>
|
||||
<option value="zoom">{% trans "Zoom Effect" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="control-label">{% trans "Interval (seconds)" %}</div>
|
||||
<input type="number" id="interval" min="5" max="60" value="15">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="control-label">{% trans "Actions" %}</div>
|
||||
<button class="btn" id="nextBtn">
|
||||
<i class="fas fa-forward"></i> {% trans "Next" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timer-display">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="timer">00:15</span>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">{% trans "Loading..." %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Application configuration
|
||||
const config = {
|
||||
animationType: 'fade',
|
||||
interval: 15000, // 15 seconds
|
||||
currentSlide: null,
|
||||
nextSlide: null,
|
||||
timer: null,
|
||||
countdown: null
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
const dom = {
|
||||
slideshow: document.getElementById('slideshow'),
|
||||
animationType: document.getElementById('animationType'),
|
||||
intervalInput: document.getElementById('interval'),
|
||||
nextBtn: document.getElementById('nextBtn'),
|
||||
loading: document.getElementById('loading'),
|
||||
timerDisplay: document.getElementById('timer'),
|
||||
progressBar: document.querySelector('.progress-bar'),
|
||||
previewItems: document.querySelectorAll('.preview-item')
|
||||
};
|
||||
|
||||
// Initialize application
|
||||
function initApp() {
|
||||
// Set event listeners
|
||||
dom.animationType.addEventListener('change', updateAnimationType);
|
||||
dom.intervalInput.addEventListener('change', updateInterval);
|
||||
dom.nextBtn.addEventListener('click', loadNextImage);
|
||||
|
||||
// Add animation preview click events
|
||||
dom.previewItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
// Remove all active classes
|
||||
dom.previewItems.forEach(i => i.classList.remove('active'));
|
||||
// Add active class to current item
|
||||
this.classList.add('active');
|
||||
// Update animation type
|
||||
config.animationType = this.dataset.animation;
|
||||
dom.animationType.value = config.animationType;
|
||||
});
|
||||
});
|
||||
|
||||
// Load first image
|
||||
loadNextImage();
|
||||
}
|
||||
|
||||
// Update animation type
|
||||
function updateAnimationType() {
|
||||
config.animationType = dom.animationType.value;
|
||||
|
||||
// Update preview selection
|
||||
dom.previewItems.forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.dataset.animation === config.animationType) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update interval
|
||||
function updateInterval() {
|
||||
const seconds = parseInt(dom.intervalInput.value);
|
||||
if (seconds >= 5 && seconds <= 60) {
|
||||
config.interval = seconds * 1000;
|
||||
resetTimer();
|
||||
} else {
|
||||
alert('{% trans "Interval must be between 5 and 60 seconds" %}');
|
||||
dom.intervalInput.value = config.interval / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset timer
|
||||
function resetTimer() {
|
||||
clearInterval(config.timer);
|
||||
clearInterval(config.countdown);
|
||||
startTimer();
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
// Start slideshow timer
|
||||
function startTimer() {
|
||||
config.timer = setInterval(loadNextImage, config.interval);
|
||||
}
|
||||
|
||||
// Start countdown display
|
||||
function startCountdown() {
|
||||
clearInterval(config.countdown);
|
||||
|
||||
let remaining = config.interval / 1000;
|
||||
updateTimerDisplay(remaining);
|
||||
|
||||
config.countdown = setInterval(() => {
|
||||
remaining--;
|
||||
|
||||
if (remaining <= 0) {
|
||||
remaining = config.interval / 1000;
|
||||
}
|
||||
|
||||
updateTimerDisplay(remaining);
|
||||
|
||||
// Update progress bar
|
||||
const progress = 100 - (remaining / (config.interval / 1000) * 100);
|
||||
dom.progressBar.style.width = `${progress}%`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Update timer display
|
||||
function updateTimerDisplay(seconds) {
|
||||
dom.timerDisplay.textContent = `00:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Get random image ID (between 1000-1084)
|
||||
function getRandomImageId() {
|
||||
return Math.floor(Math.random() * 85) + 1000;
|
||||
}
|
||||
|
||||
// Load next image
|
||||
async function loadNextImage() {
|
||||
dom.loading.style.display = 'flex';
|
||||
|
||||
try {
|
||||
// Get random image ID
|
||||
const imageId = getRandomImageId();
|
||||
const imgUrl = `https://picsum.photos/id/${imageId}/${window.innerWidth}/${window.innerHeight}`;
|
||||
|
||||
// Create new slide
|
||||
const slide = document.createElement('div');
|
||||
slide.className = `slide ${config.animationType}`;
|
||||
slide.style.backgroundImage = `url(${imgUrl})`;
|
||||
|
||||
// Add to slideshow container
|
||||
dom.slideshow.appendChild(slide);
|
||||
|
||||
// Wait for image to load
|
||||
await new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = imgUrl;
|
||||
img.onload = resolve;
|
||||
});
|
||||
|
||||
// Show new slide
|
||||
setTimeout(() => {
|
||||
// Remove current active slide
|
||||
if (config.currentSlide) {
|
||||
config.currentSlide.classList.remove('active');
|
||||
|
||||
// Remove element after animation ends
|
||||
setTimeout(() => {
|
||||
if (config.currentSlide && !config.currentSlide.classList.contains('active')) {
|
||||
config.currentSlide.remove();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Set new slide as active
|
||||
slide.classList.add('active');
|
||||
config.currentSlide = slide;
|
||||
|
||||
dom.loading.style.display = 'none';
|
||||
}, 100);
|
||||
|
||||
// Reset timer
|
||||
resetTimer();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load image:', error);
|
||||
dom.loading.innerHTML = '<div class="loading-text">{% trans "Image loading failed, please retry" %}</div>';
|
||||
setTimeout(() => {
|
||||
dom.loading.style.display = 'none';
|
||||
loadNextImage();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app when page loads
|
||||
window.addEventListener('DOMContentLoaded', initApp);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header Section -->
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||
<i class="fas fa-rocket mr-3 text-blue-600"></i>
|
||||
{% trans "Mini Apps" %}
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
{% trans "Discover a collection of powerful mini applications designed to enhance your productivity and creativity." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Apps Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-16">
|
||||
{% for app in mini_apps %}
|
||||
<div class="group relative overflow-hidden rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 mb-8">
|
||||
<!-- Thumbnail -->
|
||||
<div class="aspect-video overflow-hidden">
|
||||
<img src="{{ app.thumbnail }}"
|
||||
alt="{{ app.name }}"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300">
|
||||
</div>
|
||||
|
||||
<!-- Overlay Gradient -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="absolute inset-0 flex flex-col justify-end p-6 text-white">
|
||||
<!-- Icon and Title -->
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center mr-3"
|
||||
style="background-color: {{ app.color }};">
|
||||
<i class="{{ app.icon }} text-white text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold">{{ app.name }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-gray-200 text-sm mb-4 leading-relaxed">
|
||||
{{ app.description }}
|
||||
</p>
|
||||
|
||||
<!-- Action Button -->
|
||||
{% if app.url != "#" %}
|
||||
<a href="{% url app.url %}"
|
||||
class="inline-flex items-center justify-center px-6 py-3 rounded-lg font-medium transition-all duration-200 transform hover:scale-105"
|
||||
style="background-color: {{ app.color }};">
|
||||
<i class="fas fa-play mr-2"></i>
|
||||
{% trans "Launch App" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="inline-flex items-center justify-center px-6 py-3 rounded-lg font-medium bg-gray-600 cursor-not-allowed">
|
||||
<i class="fas fa-lock mr-2"></i>
|
||||
{% trans "Coming Soon" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Hover Effect -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-transparent via-transparent to-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.group:hover .float {
|
||||
animation: float 2s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -4,6 +4,7 @@ from . import page_views
|
||||
from . import search_views
|
||||
from . import post_views
|
||||
from . import collection_views
|
||||
from . import mini_apps_views
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
urlpatterns = [
|
||||
@@ -46,6 +47,10 @@ urlpatterns = [
|
||||
collection_views.CollectionSlideshowView.as_view(),
|
||||
name='collection-slideshow'),
|
||||
|
||||
# Mini Apps
|
||||
path('ui/mini-apps/', mini_apps_views.MiniAppsListView.as_view(), name='mini-apps-list'),
|
||||
path('ui/mini-apps/image-gallery/', mini_apps_views.ImageGalleryView.as_view(), name='mini-apps-image-gallery'),
|
||||
|
||||
# API Documentation
|
||||
path('ui/api-docs/',
|
||||
TemplateView.as_view(template_name='links/api_docs.html'),
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
source 3.12/bin/activate
|
||||
source .venv/bin/activate
|
||||
echo "Starting Django development server..."
|
||||
python manage.py runserver
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
source 3.12/bin/activate
|
||||
source .venv/bin/activate
|
||||
|
||||
echo "Starting Tailwind CSS compiler..."
|
||||
python manage.py tailwind start
|
||||
|
||||
@@ -139,6 +139,16 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'mini-apps-list' %}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 11H5m14-4H5a2 2 0 00-2 2v6a2 2 0 002 2h14M9 7h1m4 0h1M9 17h1m4 0h1"/>
|
||||
</svg>
|
||||
{% trans "Mini Apps" %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'tools' %}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
Reference in New Issue
Block a user