mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Update style of custom link page
This commit is contained in:
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse
|
||||
|
||||
class Link(models.Model):
|
||||
class LinkType(models.TextChoices):
|
||||
@@ -21,6 +22,9 @@ class Link(models.Model):
|
||||
def __str__(self):
|
||||
return self.alias
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('link_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class ClickLog(models.Model):
|
||||
link = models.ForeignKey(Link, on_delete=models.CASCADE)
|
||||
clicked_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ link.alias }}</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss-typography/0.4.0/typography.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.prose {
|
||||
color: #374151;
|
||||
max-width: 65ch;
|
||||
}
|
||||
.prose p {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.prose a {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.prose strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.prose ul {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
list-style-type: disc;
|
||||
padding-left: 1.625em;
|
||||
}
|
||||
.prose ol {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.625em;
|
||||
}
|
||||
.prose h1 {
|
||||
font-size: 2.25em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.8888889em;
|
||||
line-height: 1.1111111;
|
||||
}
|
||||
.prose h2 {
|
||||
font-size: 1.5em;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.3333333;
|
||||
}
|
||||
.prose h3 {
|
||||
font-size: 1.25em;
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 0.6em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.prose img {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.prose code {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
.prose pre {
|
||||
color: #e5e7eb;
|
||||
background-color: #1f2937;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.7142857;
|
||||
margin-top: 1.7142857em;
|
||||
margin-bottom: 1.7142857em;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.8571429em;
|
||||
padding-right: 1.1428571em;
|
||||
padding-bottom: 0.8571429em;
|
||||
padding-left: 1.1428571em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div class="max-w-3xl mx-auto mt-10 p-4 bg-white shadow-md rounded-lg">
|
||||
<div class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl max-w-none">
|
||||
{{ rendered_text|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,14 +46,14 @@
|
||||
{% for field in form %}
|
||||
{% if field.name != 'link_type' %}
|
||||
<!-- Render fields based on link type -->
|
||||
<div class="space-y-1 {% if field.name == 'original_url' %}link-field{% if is_custom %}hidden{% endif %}{% elif field.name == 'text' %}custom-field{% if not is_custom %}hidden{% endif %}{% endif %}">
|
||||
<div class="space-y-1 {% if field.name == 'original_url' %}link-field{% if is_custom %}hidden{% endif %}{% elif field.name == 'text' %} custom-field {% if not is_custom %}hidden{% endif %}{% endif %}">
|
||||
<label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700 {% if field.name == 'original_url' and is_custom or field.name == 'text' and not is_custom %}hidden{% endif %}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
{% if field.name == 'text' %}
|
||||
<!-- Render text field only for custom links -->
|
||||
{% if is_custom %}
|
||||
{% if is_custom or is_new %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
{% elif field.name == 'original_url' %}
|
||||
@@ -123,6 +123,8 @@
|
||||
const linkDescription = document.getElementById('link-description');
|
||||
const customDescription = document.getElementById('custom-description');
|
||||
const linkTypeInput = document.getElementById('id_link_type');
|
||||
const textField = document.querySelector('.custom-field');
|
||||
let simplemde;
|
||||
|
||||
function showLinkFields() {
|
||||
linkTab.classList.add('text-blue-600', 'border-blue-600');
|
||||
@@ -131,15 +133,23 @@
|
||||
customTab.classList.add('text-gray-500', 'border-transparent');
|
||||
linkFields.forEach(field => {
|
||||
field.classList.remove('hidden');
|
||||
field.querySelector('label').classList.remove('hidden');
|
||||
const label = field.querySelector('label');
|
||||
if (label) label.classList.remove('hidden');
|
||||
});
|
||||
customFields.forEach(field => {
|
||||
field.classList.add('hidden');
|
||||
field.querySelector('label').classList.add('hidden');
|
||||
const label = field.querySelector('label');
|
||||
if (label) label.classList.add('hidden');
|
||||
});
|
||||
linkDescription.classList.remove('hidden');
|
||||
customDescription.classList.add('hidden');
|
||||
linkTypeInput.value = 'LINK';
|
||||
if (simplemde) {
|
||||
//simplemde.toTextArea();
|
||||
simplemde = null;
|
||||
|
||||
}
|
||||
textField.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showCustomFields() {
|
||||
@@ -149,15 +159,28 @@
|
||||
linkTab.classList.add('text-gray-500', 'border-transparent');
|
||||
customFields.forEach(field => {
|
||||
field.classList.remove('hidden');
|
||||
field.querySelector('label').classList.remove('hidden');
|
||||
const label = field.querySelector('label');
|
||||
if (label) label.classList.remove('hidden');
|
||||
});
|
||||
linkFields.forEach(field => {
|
||||
field.classList.add('hidden');
|
||||
field.querySelector('label').classList.add('hidden');
|
||||
const label = field.querySelector('label');
|
||||
if (label) label.classList.add('hidden');
|
||||
});
|
||||
customDescription.classList.remove('hidden');
|
||||
linkDescription.classList.add('hidden');
|
||||
linkTypeInput.value = 'CUSTOM';
|
||||
initializeSimpleMDE();
|
||||
textField.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function initializeSimpleMDE() {
|
||||
if (!simplemde) {
|
||||
var textArea = document.getElementById('id_text');
|
||||
if (textArea) {
|
||||
simplemde = new SimpleMDE({ element: textArea });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
linkTab.addEventListener('click', showLinkFields);
|
||||
@@ -170,10 +193,11 @@
|
||||
showLinkFields();
|
||||
}
|
||||
|
||||
// Initialize SimpleMDE for custom links
|
||||
var textArea = document.getElementById('id_text');
|
||||
if (textArea) {
|
||||
var simplemde = new SimpleMDE({ element: textArea });
|
||||
// Check if we're creating a new link
|
||||
const isNewLink = {{ form.instance.pk|yesno:"false,true" }};
|
||||
if (isNewLink) {
|
||||
// Hide text field by default for new links
|
||||
textField.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/12">
|
||||
{% trans "Type" %}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/3">{% trans "Original URL" %}</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/12">
|
||||
<a href="?sort=clicks&order={% if current_sort == 'clicks' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
|
||||
@@ -94,6 +97,15 @@
|
||||
<td class="px-6 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-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center">
|
||||
{% if link.link_type == 'LINK' %}
|
||||
<i class="fas fa-link text-blue-500"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-code text-green-500"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="truncate max-w-xs" title="{{ link.original_url }}">
|
||||
<a href="{{ link.original_url }}" target="_blank" class="text-blue-600 hover:text-blue-900">{{ link.original_url }}</a>
|
||||
@@ -114,7 +126,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-4 whitespace-nowrap text-center text-gray-500">{% trans "No links available" %}</td>
|
||||
<td colspan="8" class="px-6 py-4 whitespace-nowrap text-center text-gray-500">{% trans "No links available" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
+28
-2
@@ -74,6 +74,11 @@ class LinkCreateView(CreateView):
|
||||
initial['alias'] = alias
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['is_new'] = True
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.alias = form.instance.alias.lower()
|
||||
alias = form.cleaned_data.get('alias').lower()
|
||||
@@ -83,6 +88,11 @@ class LinkCreateView(CreateView):
|
||||
if Link.objects.filter(alias=alias).exists():
|
||||
form.add_error('alias', _("This alias is already in use. Please choose another one."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Set original_url for custom links
|
||||
if form.cleaned_data.get('link_type') == Link.LinkType.CUSTOM:
|
||||
form.instance.original_url = f'/custom/{alias}'
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def is_valid_alias(self, alias):
|
||||
@@ -92,7 +102,6 @@ class LinkUpdateView(UpdateView):
|
||||
model = Link
|
||||
form_class = LinkForm
|
||||
template_name = 'links/link_form.html'
|
||||
success_url = reverse_lazy('link_list')
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
@@ -115,7 +124,9 @@ class LinkUpdateView(UpdateView):
|
||||
if Link.objects.filter(alias=alias).exclude(pk=self.object.pk).exists():
|
||||
form.add_error('alias', _("This alias is already in use. Please choose another one."))
|
||||
return self.form_invalid(form)
|
||||
return super().form_valid(form)
|
||||
response = super().form_valid(form)
|
||||
# Redirect to the link detail page after successful update
|
||||
return redirect(reverse('link_detail', kwargs={'pk': self.object.pk}))
|
||||
|
||||
def form_invalid(self, form):
|
||||
for field, errors in form.errors.items():
|
||||
@@ -299,3 +310,18 @@ def export_links(request):
|
||||
response = HttpResponse(json.dumps(formatted_data, indent=2), content_type='application/json')
|
||||
response['Content-Disposition'] = 'attachment; filename="links_export.json"'
|
||||
return response
|
||||
|
||||
class CustomLinkView(DetailView):
|
||||
model = Link
|
||||
template_name = 'links/custom_link.html'
|
||||
context_object_name = 'link'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
alias = self.kwargs.get('alias').lower()
|
||||
return get_object_or_404(Link, alias=alias, link_type=Link.LinkType.CUSTOM)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Render markdown text as HTML
|
||||
context['rendered_text'] = markdown.markdown(self.object.text)
|
||||
return context
|
||||
|
||||
Vendored
+18
@@ -733,6 +733,10 @@ video {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -825,6 +829,10 @@ video {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.max-w-3xl {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -1335,6 +1343,16 @@ video {
|
||||
color: rgb(22 101 52 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ from django.urls import path, include
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from links.views import LinkDetailView, LinkUpdateView, CustomLinkView
|
||||
|
||||
urlpatterns = [
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
path('search/', include('links.search_urls')),
|
||||
path('link/<int:pk>/', LinkDetailView.as_view(), name='link_detail'),
|
||||
path('link/<int:pk>/edit/', LinkUpdateView.as_view(), name='link_update'),
|
||||
path('custom/<slug:alias>/', CustomLinkView.as_view(), name='custom_link'),
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
|
||||
Reference in New Issue
Block a user