mirror of
https://github.com/rudy3333/eversync.git
synced 2025-07-01 16:46:02 +00:00
Compare commits
4 commits
6cbb408022
...
bced97db38
Author | SHA1 | Date | |
---|---|---|---|
bced97db38 | |||
970fe28e1e | |||
3c03787863 | |||
c76d6f31ef |
11 changed files with 925 additions and 27 deletions
|
@ -1,7 +1,7 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||||
from .models import Document, Event, Notes, Task
|
from .models import Document, Event, Notes, Task, WebArchive
|
||||||
from django_recaptcha.fields import ReCaptchaField
|
from django_recaptcha.fields import ReCaptchaField
|
||||||
from django_recaptcha.widgets import ReCaptchaV2Checkbox
|
from django_recaptcha.widgets import ReCaptchaV2Checkbox
|
||||||
|
|
||||||
|
@ -56,4 +56,20 @@ class TaskForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class EmailUpdateForm(forms.Form):
|
class EmailUpdateForm(forms.Form):
|
||||||
email = forms.EmailField(label="Your email", required=True)
|
email = forms.EmailField(label="Your email", required=True)
|
||||||
|
|
||||||
|
class WebArchiveForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = WebArchive
|
||||||
|
fields = ['url']
|
||||||
|
widgets = {
|
||||||
|
'url': forms.URLInput(attrs={'placeholder': 'Enter URL to archive'})
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self, commit=True, user=None):
|
||||||
|
instance = super().save(commit=False)
|
||||||
|
if user:
|
||||||
|
instance.user = user
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
return instance
|
27
eversyncc/migrations/0028_webarchive.py
Normal file
27
eversyncc/migrations/0028_webarchive.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-06-08 01:46
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('eversyncc', '0027_usernotifs'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebArchive',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('url', models.URLField()),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('screenshot', models.ImageField(upload_to='web_archives/')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
eversyncc/migrations/0029_webarchive_content.py
Normal file
18
eversyncc/migrations/0029_webarchive_content.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-06-08 02:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('eversyncc', '0028_webarchive'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='webarchive',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -113,4 +113,12 @@ class Stroke(models.Model):
|
||||||
whiteboard = models.ForeignKey(Whiteboard, related_name='strokes', on_delete=models.CASCADE)
|
whiteboard = models.ForeignKey(Whiteboard, related_name='strokes', on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
data = models.JSONField()
|
data = models.JSONField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class WebArchive(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
url = models.URLField()
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
screenshot = models.ImageField(upload_to='web_archives/')
|
||||||
|
content = models.TextField(null=True, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
@ -171,6 +171,10 @@ background-color: #03a9f4;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.service-button i {
|
.service-button i {
|
||||||
font-size: 70px;
|
font-size: 70px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
|
@ -70,13 +70,11 @@ urlpatterns = [
|
||||||
path('whiteboard/<int:whiteboard_id>/upload-image/', views.upload_image, name='upload_image'),
|
path('whiteboard/<int:whiteboard_id>/upload-image/', views.upload_image, name='upload_image'),
|
||||||
path('whiteboard/<int:whiteboard_id>/delete-image/', views.delete_image, name='delete_image'),
|
path('whiteboard/<int:whiteboard_id>/delete-image/', views.delete_image, name='delete_image'),
|
||||||
path('api/update_device_token/', views.update_device_token, name='update_device_token'),
|
path('api/update_device_token/', views.update_device_token, name='update_device_token'),
|
||||||
|
path('web_archive/', views.web_archive, name='web_archive'),
|
||||||
|
path('web_archive/save/', views.save_web_archive, name='save_web_archive'),
|
||||||
|
path('web_archive/delete/<int:archive_id>', views.delete_web_archive, name='delete_web_archive'),
|
||||||
|
path('web_archive/view/<int:archive_id>', views.view_web_archive, name='view_web_archive')
|
||||||
|
]
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
@ -3,12 +3,15 @@ from django.http import HttpResponse, JsonResponse, FileResponse
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
|
||||||
|
from django.core.files import File
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import PasswordChangeView
|
from django.contrib.auth.views import PasswordChangeView
|
||||||
from .forms import UsernameChangeForm, DocumentForm, EventForm, NoteForm, TaskForm
|
from .forms import UsernameChangeForm, DocumentForm, EventForm, NoteForm, TaskForm
|
||||||
from .models import Document, Event, Notes, Embed, Task, RichDocument, Message
|
from .models import Document, Event, Notes, Embed, Task, RichDocument, Message, WebArchive
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from allauth.account.views import LoginView as AllauthLoginView
|
from allauth.account.views import LoginView as AllauthLoginView
|
||||||
import os
|
import os
|
||||||
|
@ -17,6 +20,7 @@ from eversyncc.models import UserNotifs
|
||||||
import requests
|
import requests
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from webpush import send_user_notification
|
from webpush import send_user_notification
|
||||||
from icalendar import Calendar, Event as IcalEvent
|
from icalendar import Calendar, Event as IcalEvent
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
@ -29,7 +33,7 @@ from .embed_utils import get_embed_info
|
||||||
from .models import Whiteboard, Stroke
|
from .models import Whiteboard, Stroke
|
||||||
from eversyncc.email import verify_token
|
from eversyncc.email import verify_token
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .forms import EmailUpdateForm
|
from .forms import EmailUpdateForm, WebArchiveForm
|
||||||
from allauth.account.utils import send_email_confirmation
|
from allauth.account.utils import send_email_confirmation
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from .forms import RegisterForm
|
from .forms import RegisterForm
|
||||||
|
@ -41,6 +45,9 @@ from .models import UserNotifs
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
import selenium
|
||||||
|
|
||||||
def email_verified_required(view_func):
|
def email_verified_required(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
|
@ -1013,4 +1020,136 @@ def update_device_token(request):
|
||||||
|
|
||||||
return JsonResponse({"message": "Device token updated"})
|
return JsonResponse({"message": "Device token updated"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"error": str(e)}, status=500)
|
return JsonResponse({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
@email_verified_required
|
||||||
|
@login_required
|
||||||
|
def save_web_archive(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = WebArchiveForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
driver = None
|
||||||
|
try:
|
||||||
|
# Set up Chrome options
|
||||||
|
chrome_options = Options()
|
||||||
|
chrome_options.add_argument('--headless=new')
|
||||||
|
chrome_options.add_argument('--no-sandbox')
|
||||||
|
chrome_options.add_argument('--disable-dev-shm-usage')
|
||||||
|
chrome_options.add_argument('--disable-gpu')
|
||||||
|
chrome_options.add_argument("--lang=en-US")
|
||||||
|
chrome_options.add_experimental_option("prefs", {
|
||||||
|
"intl.accept_languages": "en,en_US",
|
||||||
|
"profile.default_content_setting_values.geolocation": 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||||
|
chrome_options.add_argument(f"user-agent={user_agent}")
|
||||||
|
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
|
|
||||||
|
# Initialize Chrome driver
|
||||||
|
driver = webdriver.Chrome(options=chrome_options)
|
||||||
|
url = form.cleaned_data['url']
|
||||||
|
print(f"Attempting to access URL: {url}")
|
||||||
|
|
||||||
|
driver.get(url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cookie_button = WebDriverWait(driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((
|
||||||
|
By.XPATH,
|
||||||
|
"//button[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||||
|
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||||
|
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'allow')]"))
|
||||||
|
)
|
||||||
|
cookie_button.click()
|
||||||
|
|
||||||
|
WebDriverWait(driver, 5).until_not(EC.presence_of_element_located((By.XPATH,
|
||||||
|
"//button[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||||
|
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||||
|
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'allow')]"))
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
print("No cookie banner found or clickable :3")
|
||||||
|
|
||||||
|
WebDriverWait(driver, 10).until(
|
||||||
|
lambda driver: driver.execute_script('return document.readyState') == 'complete'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get page title and content
|
||||||
|
title = driver.title or url
|
||||||
|
|
||||||
|
content = driver.execute_script("""
|
||||||
|
return new XMLSerializer().serializeToString(document);
|
||||||
|
""")
|
||||||
|
|
||||||
|
print(f"Page title: {title}")
|
||||||
|
|
||||||
|
# Take screenshot
|
||||||
|
screenshot = driver.get_screenshot_as_png()
|
||||||
|
print("Screenshot taken successfully")
|
||||||
|
|
||||||
|
# Create archive instance with user
|
||||||
|
archive = form.save(commit=False)
|
||||||
|
archive.user = request.user
|
||||||
|
archive.title = title
|
||||||
|
archive.content = content
|
||||||
|
archive.save()
|
||||||
|
print("Archive saved successfully")
|
||||||
|
|
||||||
|
# Now save the screenshot
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
||||||
|
tmp.write(screenshot)
|
||||||
|
print(f"Saving screenshot to: {tmp.name}")
|
||||||
|
archive.screenshot.save(f'archive_{archive.id}.png', File(open(tmp.name, 'rb')))
|
||||||
|
archive.save()
|
||||||
|
|
||||||
|
messages.success(request, "Page archived successfully!")
|
||||||
|
return redirect('web_archive')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in save_web_archive: {str(e)}")
|
||||||
|
messages.error(request, f"Error archiving page: {str(e)}")
|
||||||
|
return redirect('web_archive')
|
||||||
|
finally:
|
||||||
|
if driver:
|
||||||
|
try:
|
||||||
|
driver.quit()
|
||||||
|
print("Chrome driver closed successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing Chrome driver: {str(e)}")
|
||||||
|
else:
|
||||||
|
form = WebArchiveForm()
|
||||||
|
return render(request, 'save_web_archive.html', {'form': form})
|
||||||
|
|
||||||
|
@email_verified_required
|
||||||
|
@login_required
|
||||||
|
def web_archive(request):
|
||||||
|
archives = WebArchive.objects.filter(user=request.user)
|
||||||
|
return render(request, 'web_archive.html', {'archives': archives})
|
||||||
|
|
||||||
|
@email_verified_required
|
||||||
|
@login_required
|
||||||
|
def delete_web_archive(request, archive_id):
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
archive = get_object_or_404(WebArchive, id=archive_id, user=request.user)
|
||||||
|
|
||||||
|
if archive.screenshot:
|
||||||
|
try:
|
||||||
|
archive.screenshot.delete(save=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
archive.delete()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Error deleting archive: {str(e)}")
|
||||||
|
|
||||||
|
return redirect('web_archive')
|
||||||
|
|
||||||
|
@email_verified_required
|
||||||
|
@login_required
|
||||||
|
def view_web_archive(request, archive_id):
|
||||||
|
archive = get_object_or_404(WebArchive, id=archive_id, user=request.user)
|
||||||
|
return render(request, 'view_web_archive.html', {'archive': archive})
|
||||||
|
|
||||||
|
|
36
index.html
36
index.html
|
@ -176,23 +176,29 @@
|
||||||
<a href="/documents" style="text-decoration: none;"><div class="service-button documents">
|
<a href="/documents" style="text-decoration: none;"><div class="service-button documents">
|
||||||
<i class="fa-solid fa-file"></i>
|
<i class="fa-solid fa-file"></i>
|
||||||
documents
|
documents
|
||||||
</div></a>
|
</div></a>
|
||||||
|
|
||||||
<a href="/chat" style="text-decoration: none;"><div class="service-button chat">
|
<a href="/chat" style="text-decoration: none;"><div class="service-button chat">
|
||||||
<i class="fa-solid fa-comment"></i>
|
<i class="fa-solid fa-comment"></i>
|
||||||
chat
|
chat
|
||||||
</div></a>
|
</div></a>
|
||||||
|
|
||||||
<a href="/music" style="text-decoration: none;"><div class="service-button music">
|
|
||||||
<i class="fa-solid fa-music"></i>
|
|
||||||
music
|
|
||||||
</div></a>
|
|
||||||
|
|
||||||
<a href="/whiteboard" style="text-decoration: none;"><div class="service-button whiteboard">
|
|
||||||
<i class="fa-solid fa-chalkboard-user"></i>
|
|
||||||
whiteboard
|
|
||||||
</div></a>
|
|
||||||
|
|
||||||
|
<a href="/music" style="text-decoration: none;"><div class="service-button music">
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
music
|
||||||
|
</div></a>
|
||||||
|
|
||||||
|
<a href="/whiteboard" style="text-decoration: none;"><div class="service-button whiteboard">
|
||||||
|
<i class="fa-solid fa-chalkboard-user"></i>
|
||||||
|
whiteboard
|
||||||
|
</div></a>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<a href="/web_archive" style="text-decoration: none;"><div class="service-button archive">
|
||||||
|
<i class="fa-solid fa-camera"></i>
|
||||||
|
web archive
|
||||||
|
</div></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
191
templates/save_web_archive.html
Normal file
191
templates/save_web_archive.html
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Eversync</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||||
|
{% load static %}
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'index-style.css' %}">
|
||||||
|
{% block scripts %}
|
||||||
|
{% include "sentry_replay.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-box {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100vw;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.welcome-box {
|
||||||
|
flex: none;
|
||||||
|
width: auto;
|
||||||
|
max-width: 90vw;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="/"><img src="{% static 'eversync2.png' %}" alt="Eversync Logo" style="height: 80px; margin-right: 10px; display: flex; align-items: center; gap: 5px;"></a>
|
||||||
|
<a href="/" class="logo" >eversync</a>
|
||||||
|
<div class="nav-links" style="position: relative;">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="dropdown-toggle" style="background: none; border: none; color: white; font-size: 16px; cursor: pointer;">
|
||||||
|
Welcome, {{ user.username }} <i class="fas fa-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" style="display: none; position: absolute; right: 0; background-color: #333; border: 1px solid #444; border-radius: 4px; padding: 10px; width: 184px;">
|
||||||
|
<form action="{% url 'manage' %}" method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Manage Account</button>
|
||||||
|
</form>
|
||||||
|
<form action="{% url 'logoutz' %}" method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Log Out</button>
|
||||||
|
</form>
|
||||||
|
<button id="themeToggle" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Toggle Dark Mode</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="welcome-box">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Archive Web Page</h1>
|
||||||
|
<a href="{% url 'web_archive' %}" class="btn btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left"></i> Back to Archives
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<form method="POST" class="archive-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.url.id_for_label }}">URL to Archive</label>
|
||||||
|
{{ form.url }}
|
||||||
|
{% if form.url.errors %}
|
||||||
|
<div class="error-message">{{ form.url.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fa-solid fa-camera"></i> Take Screenshot
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const toggle = document.querySelector('.dropdown-toggle');
|
||||||
|
const menu = document.querySelector('.dropdown-menu');
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = document.getElementById("themeToggle");
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
if (localStorage.getItem("theme") === "dark") {
|
||||||
|
root.classList.add("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
root.classList.toggle("dark");
|
||||||
|
if (root.classList.contains("dark")) {
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
264
templates/view_web_archive.html
Normal file
264
templates/view_web_archive.html
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Eversync</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||||
|
{% load static %}
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'index-style.css' %}">
|
||||||
|
<style>
|
||||||
|
.archive-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.archive-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.archive-title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.archive-meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.archive-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.archive-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.archive-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 800px;
|
||||||
|
border: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.view-toggle {
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.raw-html {
|
||||||
|
display: none;
|
||||||
|
background: #282c34;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.raw-html.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.archive-frame.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.archive-frame.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.html-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.html-controls button {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.html-controls button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.code-block {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.hljs {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="/"><img src="{% static 'eversync2.png' %}" alt="Eversync Logo" style="height: 80px; margin-right: 10px; display: flex; align-items: center; gap: 5px;"></a>
|
||||||
|
<a href="/" class="logo" >eversync</a>
|
||||||
|
<div class="nav-links" style="position: relative;">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="dropdown-toggle" style="background: none; border: none; color: white; font-size: 16px; cursor: pointer;">
|
||||||
|
Welcome, {{ user.username }} <i class="fas fa-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" style="display: none; position: absolute; right: 0; background-color: #333; border: 1px solid #444; border-radius: 4px; padding: 10px; width: 184px;">
|
||||||
|
|
||||||
|
<form action="{% url 'manage' %}" method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Manage Account</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{% url 'logoutz' %}" method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Log Out</button>
|
||||||
|
</form>
|
||||||
|
<button id="themeToggle" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Toggle Dark Mode</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="archive-container">
|
||||||
|
<div class="archive-header">
|
||||||
|
<div>
|
||||||
|
<h1 class="archive-title">{{ archive.title }}</h1>
|
||||||
|
<p class="archive-meta">
|
||||||
|
Archived on {{ archive.created_at|date:"F j, Y" }} from
|
||||||
|
<a href="{{ archive.url }}" target="_blank">{{ archive.url }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="archive-actions">
|
||||||
|
<a href="{% url 'web_archive' %}" class="btn btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left"></i> Back to Archives
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if archive.screenshot %}
|
||||||
|
<img src="{{ archive.screenshot.url }}" alt="Archive screenshot" style="max-width: 100%; height: auto; margin-bottom: 20px;">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button class="btn btn-primary" onclick="toggleView()">
|
||||||
|
<i class="fa-solid fa-code"></i> Toggle Raw HTML
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="copyHtml()">
|
||||||
|
<i class="fa-solid fa-copy"></i> Copy HTML
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="archive-content">
|
||||||
|
<iframe srcdoc="{{ archive.content }}" class="archive-frame"></iframe>
|
||||||
|
<div class="raw-html">
|
||||||
|
<div class="html-controls">
|
||||||
|
<button onclick="copyHtml()">
|
||||||
|
<i class="fa-solid fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
<button onclick="downloadHtml()">
|
||||||
|
<i class="fa-solid fa-download"></i> Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre class="code-block"><code class="language-html">{{ archive.content }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function toggleView() {
|
||||||
|
const iframe = document.querySelector('.archive-frame');
|
||||||
|
const rawHtml = document.querySelector('.raw-html');
|
||||||
|
|
||||||
|
iframe.classList.toggle('active');
|
||||||
|
iframe.classList.toggle('hidden');
|
||||||
|
rawHtml.classList.toggle('active');
|
||||||
|
|
||||||
|
if (rawHtml.classList.contains('active')) {
|
||||||
|
hljs.highlightAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyHtml() {
|
||||||
|
const htmlContent = `{{ archive.content|escapejs }}`;
|
||||||
|
navigator.clipboard.writeText(htmlContent).then(() => {
|
||||||
|
alert('HTML copied to clipboard!');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy HTML:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadHtml() {
|
||||||
|
const htmlContent = `{{ archive.content|escapejs }}`;
|
||||||
|
const blob = new Blob([htmlContent], { type: 'text/html' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'archive.html';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize syntax highlighting when the page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
|
hljs.highlightAll();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const toggle = document.querySelector('.dropdown-toggle');
|
||||||
|
const menu = document.querySelector('.dropdown-menu');
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = document.getElementById("themeToggle");
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
if (localStorage.getItem("theme") === "dark") {
|
||||||
|
root.classList.add("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
root.classList.toggle("dark");
|
||||||
|
if (root.classList.contains("dark")) {
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
227
templates/web_archive.html
Normal file
227
templates/web_archive.html
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Eversync</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||||
|
{% load static %}
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'index-style.css' %}">
|
||||||
|
{% block scripts %}
|
||||||
|
{% include "sentry_replay.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-box {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 900px;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.archives-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-screenshot {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-url {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 5px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-archives {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.welcome-box {
|
||||||
|
flex: none;
|
||||||
|
width: auto;
|
||||||
|
max-width: 90vw;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.archives-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="/"><img src="{% static 'eversync2.png' %}" alt="Eversync Logo" style="height: 80px; margin-right: 10px; display: flex; align-items: center; gap: 5px;"></a>
|
||||||
|
<a href="/" class="logo" >eversync</a>
|
||||||
|
<div class="nav-links" style="position: relative;">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="dropdown-toggle" style="background: none; border: none; color: white; font-size: 16px; cursor: pointer;">
|
||||||
|
Welcome, {{ user.username }} <i class="fas fa-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" style="display: none; position: absolute; right: 0; background-color: #333; border: 1px solid #444; border-radius: 4px; padding: 10px; width: 184px;">
|
||||||
|
|
||||||
|
<form action="{% url 'manage' %}" method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Manage Account</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{% url 'logoutz' %}" method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Log Out</button>
|
||||||
|
</form>
|
||||||
|
<button id="themeToggle" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Toggle Dark Mode</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="welcome-box">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Web Archives</h1>
|
||||||
|
<a href="{% url 'save_web_archive' %}" class="btn btn-primary">
|
||||||
|
<i class="fa-solid fa-plus"></i> New Archive
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="archives-grid">
|
||||||
|
{% for archive in archives %}
|
||||||
|
<div class="archive-card">
|
||||||
|
{% if archive.screenshot %}
|
||||||
|
<img src="{{ archive.screenshot.url }}" alt="Archived page" class="archive-screenshot">
|
||||||
|
{% endif %}
|
||||||
|
<div class="archive-content">
|
||||||
|
<p class="archive-url">{{ archive.url }}</p>
|
||||||
|
<p class="archive-date">{{ archive.created_at|date:"F j, Y" }}</p>
|
||||||
|
<div class="archive-actions">
|
||||||
|
<a href="{% url 'view_web_archive' archive.id %}" class="btn btn-primary">
|
||||||
|
<i class="fa-solid fa-eye"></i> View
|
||||||
|
</a>
|
||||||
|
<a href="{{ archive.url }}" target="_blank" class="btn btn-secondary">
|
||||||
|
<i class="fa-solid fa-external-link"></i> Visit
|
||||||
|
</a>
|
||||||
|
<form action="{% url 'delete_web_archive' archive.id %}" method="post" style="display: inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this archive?')">
|
||||||
|
<i class="fa-solid fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="no-archives">
|
||||||
|
<p>No archived pages yet. Start by archiving your first page!</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const toggle = document.querySelector('.dropdown-toggle');
|
||||||
|
const menu = document.querySelector('.dropdown-menu');
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = document.getElementById("themeToggle");
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
if (localStorage.getItem("theme") === "dark") {
|
||||||
|
root.classList.add("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
root.classList.toggle("dark");
|
||||||
|
if (root.classList.contains("dark")) {
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue