Compare commits

...

4 commits

11 changed files with 925 additions and 27 deletions

View file

@ -1,7 +1,7 @@
from django import forms
from django.contrib.auth.models import User
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.widgets import ReCaptchaV2Checkbox
@ -57,3 +57,19 @@ class TaskForm(forms.ModelForm):
class EmailUpdateForm(forms.Form):
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

View 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)),
],
),
]

View 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),
),
]

View file

@ -114,3 +114,11 @@ class Stroke(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
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)

View file

@ -171,6 +171,10 @@ background-color: #03a9f4;
background-color: white;
}
.archive {
background-color: white;
}
.service-button i {
font-size: 70px;
margin-bottom: 5px;

View file

@ -70,12 +70,10 @@ urlpatterns = [
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('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:

View file

@ -3,12 +3,15 @@ from django.http import HttpResponse, JsonResponse, FileResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
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.
from django.contrib.auth import logout
from django.contrib.auth.models import User
from django.contrib.auth.views import PasswordChangeView
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 allauth.account.views import LoginView as AllauthLoginView
import os
@ -17,6 +20,7 @@ from eversyncc.models import UserNotifs
import requests
from allauth.account.models import EmailAddress
from django.views.decorators.clickjacking import xframe_options_exempt
from selenium.webdriver.support.ui import WebDriverWait
from webpush import send_user_notification
from icalendar import Calendar, Event as IcalEvent
from django.utils.text import slugify
@ -29,7 +33,7 @@ from .embed_utils import get_embed_info
from .models import Whiteboard, Stroke
from eversyncc.email import verify_token
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 functools import wraps
from .forms import RegisterForm
@ -41,6 +45,9 @@ from .models import UserNotifs
import aiohttp
import asyncio
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):
@wraps(view_func)
@ -1014,3 +1021,135 @@ def update_device_token(request):
return JsonResponse({"message": "Device token updated"})
except Exception as e:
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})

View file

@ -192,7 +192,13 @@
<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>

View 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>

View 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
View 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>