mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Update style on desktop browser
This commit is contained in:
@@ -1,7 +1,221 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Desktop-specific responsive improvements */
|
||||
@media (min-width: 1024px) {
|
||||
.main-content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.random-images-desktop {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.random-image-container {
|
||||
aspect-ratio: 4/3;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.popular-links-desktop {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.latest-posts-desktop {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.latest-post-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.latest-post-card:hover {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile and tablet - keep original layout */
|
||||
@media (max-width: 1023px) {
|
||||
.desktop-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-layout {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop - use new layout */
|
||||
@media (min-width: 1024px) {
|
||||
.mobile-layout {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Desktop Layout (1024px and up) -->
|
||||
<div class="desktop-only">
|
||||
<div class="main-content-grid">
|
||||
<!-- Left Column - Main Content -->
|
||||
<div class="left-column">
|
||||
<!-- Popular Links Section -->
|
||||
<div>
|
||||
<h2 class="section-title">{% trans "Popular Links" %}</h2>
|
||||
<div class="popular-links-desktop">
|
||||
{% for item in popular_links %}
|
||||
<div class="rounded-lg shadow-md overflow-hidden" style="background-color: {{ item.color }};">
|
||||
<div class="p-4">
|
||||
<a href="{% url 'redirect_to_original' item.link.alias %}" class="text-white hover:text-gray-200 font-semibold text-lg block truncate">
|
||||
{{ item.link.alias }}
|
||||
</a>
|
||||
<p class="text-white text-sm mt-2 truncate">{% trans "Clicks" %}: {{ item.link.click_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-gray-600 col-span-full">{% trans "No popular links available" %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Latest Posts Section -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h2 class="section-title mb-0">{% trans "Latest Posts" %}</h2>
|
||||
<a href="{% url 'post-list' %}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">{% trans "View all posts" %} →</a>
|
||||
</div>
|
||||
{% if latest_posts %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for post in latest_posts %}
|
||||
<div class="latest-post-card">
|
||||
<h3 class="font-semibold text-gray-800 text-base mb-2 line-clamp-2">
|
||||
<a href="{{ post.get_absolute_url }}" class="hover:text-blue-600 transition-colors">
|
||||
{{ post.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<span>{{ post.created_at|date:"M d, Y" }}</span>
|
||||
{% if post.tags.all %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for tag in post.tags.all|slice:":1" %}
|
||||
<span class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-center">
|
||||
<p class="text-gray-600 text-sm">{% trans "No posts available yet" %}</p>
|
||||
<a href="{% url 'post-create' %}" class="inline-block mt-2 text-blue-600 hover:text-blue-800 font-medium text-sm">{% trans "Create your first post" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Pinned Mini Apps Section -->
|
||||
<div>
|
||||
<h2 class="section-title">{% trans "Pinned Mini Apps" %}</h2>
|
||||
<div class="popular-links-desktop">
|
||||
{% for app in mini_apps %}
|
||||
<div class="rounded-lg shadow-md overflow-hidden" style="background-color: {{ app.color }};">
|
||||
<div class="p-4">
|
||||
{% if app.url != '#' %}
|
||||
<a href="{% url app.url %}" class="text-white hover:text-gray-200 font-semibold text-lg block truncate">
|
||||
{{ app.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-white font-semibold text-lg block truncate cursor-default">
|
||||
{{ app.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<p class="text-white text-sm mt-2 truncate">{{ app.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-gray-600 col-span-full">{% trans "No mini apps available" %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Sidebar -->
|
||||
<div class="right-column">
|
||||
<!-- Random Images Section -->
|
||||
<div>
|
||||
<h2 class="section-title">{% trans "Random Images" %}</h2>
|
||||
<div class="random-images-desktop">
|
||||
<!-- Random Image Box 1 -->
|
||||
<div class="random-image-container bg-gray-100 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-200">
|
||||
<img
|
||||
src="/api/images/random/400/300/?fit=crop&v={{ timestamp }}"
|
||||
alt="{% trans 'Random Image' %}"
|
||||
class="w-full h-full object-cover cursor-pointer hover:scale-105 transition-transform duration-200 random-image"
|
||||
data-box="1"
|
||||
onclick="refreshRandomImage(this)"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Random Image Box 2 -->
|
||||
<div class="random-image-container bg-gray-100 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-200">
|
||||
<img
|
||||
src="/api/images/random/400/300/?fit=crop&v={{ timestamp }}&offset=1"
|
||||
alt="{% trans 'Random Image' %}"
|
||||
class="w-full h-full object-cover cursor-pointer hover:scale-105 transition-transform duration-200 random-image"
|
||||
data-box="2"
|
||||
onclick="refreshRandomImage(this)"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile/Tablet Layout (below 1024px) - Original Layout -->
|
||||
<div class="mobile-layout">
|
||||
<!-- Popular Links Section -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-700 mb-3 uppercase tracking-wide text-sm">{% trans "Popular Links" %}</h2>
|
||||
@@ -113,253 +327,254 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<div class="p-4 sm:p-6 bg-gray-50 border-b border-gray-200">
|
||||
<h1 class="text-lg font-semibold text-gray-700 mb-3 uppercase tracking-wide text-sm">{% trans "Link List" %}</h1>
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0">
|
||||
<div class="w-full sm:w-64 relative">
|
||||
<input type="text" id="search-alias-input" placeholder="{% trans 'Search links...' %}" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<svg class="h-4 w-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'link_create' %}" class="w-full sm:w-auto bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium py-2 px-4 rounded inline-flex items-center justify-center transition duration-150 ease-in-out">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
<!-- Shared Link List Section (both layouts) -->
|
||||
<div class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<div class="p-4 sm:p-6 bg-gray-50 border-b border-gray-200">
|
||||
<h1 class="text-lg font-semibold text-gray-700 mb-3 uppercase tracking-wide text-sm">{% trans "Link List" %}</h1>
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0">
|
||||
<div class="w-full sm:w-64 relative">
|
||||
<input type="text" id="search-alias-input" placeholder="{% trans 'Search links...' %}" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<svg class="h-4 w-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% trans "New Link" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'link_create' %}" class="w-full sm:w-auto bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium py-2 px-4 rounded inline-flex items-center justify-center transition duration-150 ease-in-out">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
{% trans "New Link" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'delete_selected' %}">
|
||||
{% csrf_token %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<input type="checkbox" id="select-all" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<a href="?sort=alias&order={% if current_sort == 'alias' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Alias" %}
|
||||
{% if current_sort == 'alias' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">
|
||||
<a href="?sort=link_type&order={% if current_sort == 'link_type' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Type" %}
|
||||
{% if current_sort == 'link_type' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">
|
||||
<a href="?sort=original_url&order={% if current_sort == 'original_url' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Original URL" %}
|
||||
{% if current_sort == 'original_url' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">
|
||||
<a href="?sort=clicks&order={% if current_sort == 'clicks' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Clicks" %}
|
||||
{% if current_sort == 'clicks' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200" id="links-table-body">
|
||||
{% for link in links %}
|
||||
<tr class="link-row">
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap">
|
||||
<input type="checkbox" name="selected_links" value="{{ link.id }}" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap">
|
||||
<a href="{% url 'redirect_to_original' link.alias %}" target="_blank" class="text-blue-600 hover:text-blue-900">{{ link.alias }}</a>
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<span class="inline-flex items-center">
|
||||
{% if '{' in link.original_url and '}' in link.original_url %}
|
||||
<i class="fas fa-magic text-purple-500"></i>
|
||||
<span class="ml-2 text-sm text-purple-600">Template</span>
|
||||
{% elif link.link_type == 'LINK' %}
|
||||
<i class="fas fa-link text-blue-500"></i>
|
||||
<span class="ml-2 text-sm text-blue-600">Link</span>
|
||||
{% else %}
|
||||
<i class="fas fa-code text-green-500"></i>
|
||||
<span class="ml-2 text-sm text-green-600">Custom</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap hidden md:table-cell">
|
||||
<div class="truncate max-w-xs" title="{{ link.original_url }}">
|
||||
<a href="{% url 'redirect_to_original' link.alias %}"
|
||||
class="text-blue-600 hover:text-blue-800 break-all"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
{{ link.original_url }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap hidden sm:table-cell">{{ link.click_count }}</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<a href="{% url 'link_detail' link.id %}"
|
||||
class="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded"
|
||||
title="{% trans 'View Details' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'link_update' link.id %}"
|
||||
class="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded"
|
||||
title="{% trans 'Edit' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'link_delete' link.id %}"
|
||||
class="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded"
|
||||
title="{% trans 'Delete' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap text-center text-gray-500">{% trans "No links available" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">{% trans "Delete Selected" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
<form method="post" action="{% url 'delete_selected' %}">
|
||||
{% csrf_token %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<input type="checkbox" id="select-all" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<a href="?sort=alias&order={% if current_sort == 'alias' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Alias" %}
|
||||
{% if current_sort == 'alias' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">
|
||||
<a href="?sort=link_type&order={% if current_sort == 'link_type' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Type" %}
|
||||
{% if current_sort == 'link_type' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">
|
||||
<a href="?sort=original_url&order={% if current_sort == 'original_url' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Original URL" %}
|
||||
{% if current_sort == 'original_url' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">
|
||||
<a href="?sort=clicks&order={% if current_sort == 'clicks' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
{% trans "Clicks" %}
|
||||
{% if current_sort == 'clicks' %}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M{% if current_order == 'asc' %}19 9l-7 7-7-7{% else %}5 15l7-7 7 7{% endif %}"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200" id="links-table-body">
|
||||
{% for link in links %}
|
||||
<tr class="link-row">
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap">
|
||||
<input type="checkbox" name="selected_links" value="{{ link.id }}" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap">
|
||||
<a href="{% url 'redirect_to_original' link.alias %}" target="_blank" class="text-blue-600 hover:text-blue-900">{{ link.alias }}</a>
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<span class="inline-flex items-center">
|
||||
{% if '{' in link.original_url and '}' in link.original_url %}
|
||||
<i class="fas fa-magic text-purple-500"></i>
|
||||
<span class="ml-2 text-sm text-purple-600">Template</span>
|
||||
{% elif link.link_type == 'LINK' %}
|
||||
<i class="fas fa-link text-blue-500"></i>
|
||||
<span class="ml-2 text-sm text-blue-600">Link</span>
|
||||
{% else %}
|
||||
<i class="fas fa-code text-green-500"></i>
|
||||
<span class="ml-2 text-sm text-green-600">Custom</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap hidden md:table-cell">
|
||||
<div class="truncate max-w-xs" title="{{ link.original_url }}">
|
||||
<a href="{% url 'redirect_to_original' link.alias %}"
|
||||
class="text-blue-600 hover:text-blue-800 break-all"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
{{ link.original_url }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap hidden sm:table-cell">{{ link.click_count }}</td>
|
||||
<td class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<a href="{% url 'link_detail' link.id %}"
|
||||
class="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded"
|
||||
title="{% trans 'View Details' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'link_update' link.id %}"
|
||||
class="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded"
|
||||
title="{% trans 'Edit' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'link_delete' link.id %}"
|
||||
class="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded"
|
||||
title="{% trans 'Delete' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-2 sm:px-6 sm:py-4 whitespace-nowrap text-center text-gray-500">{% trans "No links available" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">{% trans "Delete Selected" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
const searchInput = document.getElementById('search-alias-input');
|
||||
const rows = document.querySelectorAll('.link-row');
|
||||
const tbody = document.getElementById('links-table-body');
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const searchInput = document.getElementById('search-alias-input');
|
||||
const rows = document.querySelectorAll('.link-row');
|
||||
const tbody = document.getElementById('links-table-body');
|
||||
|
||||
|
||||
if (searchInput) {
|
||||
if (searchInput) {
|
||||
|
||||
function performSearch() {
|
||||
const searchTerm = searchInput.value.toLowerCase().trim();
|
||||
let visibleCount = 0;
|
||||
function performSearch() {
|
||||
const searchTerm = searchInput.value.toLowerCase().trim();
|
||||
let visibleCount = 0;
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
const aliasElement = row.querySelector('td:nth-child(2) a');
|
||||
if (aliasElement) {
|
||||
const alias = aliasElement.textContent.toLowerCase();
|
||||
const shouldShow = alias.includes(searchTerm);
|
||||
row.style.display = shouldShow ? '' : 'none';
|
||||
if (shouldShow) visibleCount++;
|
||||
} else {
|
||||
console.error(`第 ${index + 1} 行没有找到别名元素`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const noResultsRow = document.getElementById('no-results-row');
|
||||
if (visibleCount === 0) {
|
||||
if (!noResultsRow) {
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.id = 'no-results-row';
|
||||
newRow.innerHTML = `<td colspan="8" class="px-6 py-4 whitespace-nowrap text-center text-gray-500">{% trans "No matching links found" %}</td>`;
|
||||
tbody.appendChild(newRow);
|
||||
}
|
||||
} else if (noResultsRow) {
|
||||
noResultsRow.remove();
|
||||
rows.forEach((row, index) => {
|
||||
const aliasElement = row.querySelector('td:nth-child(2) a');
|
||||
if (aliasElement) {
|
||||
const alias = aliasElement.textContent.toLowerCase();
|
||||
const shouldShow = alias.includes(searchTerm);
|
||||
row.style.display = shouldShow ? '' : 'none';
|
||||
if (shouldShow) visibleCount++;
|
||||
} else {
|
||||
console.error(`第 ${index + 1} 行没有找到别名元素`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const noResultsRow = document.getElementById('no-results-row');
|
||||
if (visibleCount === 0) {
|
||||
if (!noResultsRow) {
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.id = 'no-results-row';
|
||||
newRow.innerHTML = `<td colspan="8" class="px-6 py-4 whitespace-nowrap text-center text-gray-500">{% trans "No matching links found" %}</td>`;
|
||||
tbody.appendChild(newRow);
|
||||
}
|
||||
} else if (noResultsRow) {
|
||||
noResultsRow.remove();
|
||||
}
|
||||
|
||||
// 使用 'keyup' 事件
|
||||
searchInput.addEventListener('keyup', performSearch);
|
||||
} else {
|
||||
console.error('未找到搜索输入框');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const utcTimes = document.querySelectorAll('.utc-time');
|
||||
utcTimes.forEach(function(element) {
|
||||
const utcTime = new Date(element.dataset.utc);
|
||||
const localTime = utcTime.toLocaleString();
|
||||
element.textContent = localTime;
|
||||
});
|
||||
});
|
||||
|
||||
// Random image refresh functionality
|
||||
function refreshRandomImage(imgElement) {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Determine if it's mobile or desktop for different sizes
|
||||
const isMobile = window.innerWidth < 640; // sm breakpoint
|
||||
let newUrl;
|
||||
|
||||
if (imgElement.dataset.box === '1') {
|
||||
// First image box
|
||||
const size = isMobile ? '250' : '300';
|
||||
newUrl = `/api/images/random/${size}/${size}/?fit=crop&v=${timestamp}`;
|
||||
} else {
|
||||
// Second image box
|
||||
const size = isMobile ? '280' : '400';
|
||||
newUrl = `/api/images/random/${size}/${size}/?fit=crop&v=${timestamp}&offset=1`;
|
||||
}
|
||||
|
||||
// Add loading animation
|
||||
imgElement.style.opacity = '0.5';
|
||||
imgElement.style.transition = 'opacity 0.3s ease';
|
||||
|
||||
const newImg = new Image();
|
||||
newImg.onload = function() {
|
||||
imgElement.src = newImg.src;
|
||||
imgElement.style.opacity = '1';
|
||||
};
|
||||
newImg.onerror = function() {
|
||||
// Fallback - just update timestamp on current URL
|
||||
const currentUrl = new URL(imgElement.src);
|
||||
currentUrl.searchParams.set('v', timestamp);
|
||||
imgElement.src = currentUrl.toString();
|
||||
imgElement.style.opacity = '1';
|
||||
};
|
||||
newImg.src = newUrl;
|
||||
// 使用 'keyup' 事件
|
||||
searchInput.addEventListener('keyup', performSearch);
|
||||
} else {
|
||||
console.error('未找到搜索输入框');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const utcTimes = document.querySelectorAll('.utc-time');
|
||||
utcTimes.forEach(function(element) {
|
||||
const utcTime = new Date(element.dataset.utc);
|
||||
const localTime = utcTime.toLocaleString();
|
||||
element.textContent = localTime;
|
||||
});
|
||||
});
|
||||
|
||||
// Random image refresh functionality
|
||||
function refreshRandomImage(img) {
|
||||
const box = img.getAttribute('data-box');
|
||||
const timestamp = new Date().getTime();
|
||||
const width = img.offsetWidth || 500;
|
||||
const height = img.offsetHeight || 500;
|
||||
const newUrl = `/api/images/random/${width}/${height}/?fit=crop&v=${timestamp}&box=${box}`;
|
||||
|
||||
// Create new image to preload
|
||||
const newImg = new Image();
|
||||
newImg.onload = function() {
|
||||
img.style.opacity = '0.5';
|
||||
setTimeout(() => {
|
||||
img.src = newUrl;
|
||||
img.style.opacity = '1';
|
||||
}, 150);
|
||||
};
|
||||
newImg.onerror = function() {
|
||||
console.error('Failed to load new random image');
|
||||
// Fallback - just update timestamp on current URL
|
||||
const currentUrl = new URL(img.src);
|
||||
currentUrl.searchParams.set('v', timestamp);
|
||||
img.src = currentUrl.toString();
|
||||
img.style.opacity = '1';
|
||||
};
|
||||
newImg.src = newUrl;
|
||||
}
|
||||
|
||||
// Select all functionality
|
||||
document.getElementById('select-all').addEventListener('change', function() {
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_links"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = this.checked;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user