initial commit
This commit is contained in:
215
static/css/styles.css
Normal file
215
static/css/styles.css
Normal file
@@ -0,0 +1,215 @@
|
||||
/* Custom styles for Django Unfold admin */
|
||||
:root {
|
||||
--primary-color: #4f46e5;
|
||||
--primary-hover: #4338ca;
|
||||
}
|
||||
|
||||
.unfold-admin .button-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.unfold-admin .button-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* Persian/RTL Support */
|
||||
html[lang="fa"],
|
||||
html[dir="rtl"] {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
html[lang="fa"] body,
|
||||
html[dir="rtl"] body {
|
||||
font-family: "Vazir", "Tahoma", "Arial", sans-serif;
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* RTL adjustments for admin interface */
|
||||
html[lang="fa"] .unfold-admin,
|
||||
html[dir="rtl"] .unfold-admin {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
html[lang="fa"] .unfold-admin .sidebar,
|
||||
html[dir="rtl"] .unfold-admin .sidebar {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
html[lang="fa"] .unfold-admin .main-content,
|
||||
html[dir="rtl"] .unfold-admin .main-content {
|
||||
margin-right: 250px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Persian number support */
|
||||
html[lang="fa"] .persian-numbers {
|
||||
font-family: "Vazir", monospace;
|
||||
}
|
||||
|
||||
/* Custom styles for image previews */
|
||||
.image-preview {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Persian font loading */
|
||||
@font-face {
|
||||
font-family: "Vazir";
|
||||
src: url("https://cdn.jsdelivr.net/gh/rastikerdar/vazir-font@v30.1.0/dist/Vazir-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Vazir";
|
||||
src: url("https://cdn.jsdelivr.net/gh/rastikerdar/vazir-font@v30.1.0/dist/Vazir-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
|
||||
/* --- MarkdownX / SimpleMDE Preview Overrides --- */
|
||||
/* Target the preview pane itself */
|
||||
.editor-preview-side, .editor-preview {
|
||||
background-color: #ffffff; /* Ensure white background */
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow-x: auto; /* For wide content like code blocks */
|
||||
line-height: 1.6; /* Standard line height */
|
||||
color: #333; /* Default text color */
|
||||
}
|
||||
|
||||
/* Reset common text elements */
|
||||
.editor-preview-side h1, .editor-preview h1,
|
||||
.editor-preview-side h2, .editor-preview h2,
|
||||
.editor-preview-side h3, .editor-preview h3,
|
||||
.editor-preview-side h4, .editor-preview h4,
|
||||
.editor-preview-side h5, .editor-preview h5,
|
||||
.editor-preview-side h6, .editor-preview h6 {
|
||||
font-family: inherit; /* Use default font */
|
||||
color: inherit; /* Use default color */
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-preview-side h1, .editor-preview h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
.editor-preview-side h2, .editor-preview h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.editor-preview-side h3, .editor-preview h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
.editor-preview-side h4, .editor-preview h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
.editor-preview-side h5, .editor-preview h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
.editor-preview-side h6, .editor-preview h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
.editor-preview-side p, .editor-preview p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.editor-preview-side ul, .editor-preview ul,
|
||||
.editor-preview-side ol, .editor-preview ol {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.editor-preview-side li, .editor-preview li {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.editor-preview-side > ol > li, .editor-preview > ol > li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.editor-preview-side pre, .editor-preview pre {
|
||||
background-color: #f4f4f4;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.editor-preview-side code, .editor-preview code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
background-color: rgba(27, 31, 35, 0.05);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.editor-preview-side table, .editor-preview table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.editor-preview-side th, .editor-preview th,
|
||||
.editor-preview-side td, .editor-preview td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.editor-preview-side th, .editor-preview th {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
.editor-preview-side blockquote, .editor-preview blockquote {
|
||||
border-left: 4px solid #ccc;
|
||||
padding-left: 15px;
|
||||
color: #666;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
.editor-preview-side img, .editor-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block; /* Prevent extra space below image */
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.editor-preview-side a, .editor-preview a {
|
||||
color: #0366d6; /* Standard link color */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-preview-side a:hover, .editor-preview a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Horizontal Rule */
|
||||
.editor-preview-side hr, .editor-preview hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: #eee;
|
||||
margin: 1em 0;
|
||||
}
|
||||
BIN
static/img/logo.png
Normal file
BIN
static/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
182
static/js/push-notifications.js
Normal file
182
static/js/push-notifications.js
Normal file
@@ -0,0 +1,182 @@
|
||||
// Push Notifications Client-side JavaScript
|
||||
class PushNotificationManager {
|
||||
constructor() {
|
||||
this.vapidPublicKey = null
|
||||
this.serviceWorkerRegistration = null
|
||||
this.isSupported = "serviceWorker" in navigator && "PushManager" in window
|
||||
}
|
||||
|
||||
async init(vapidPublicKey) {
|
||||
if (!this.isSupported) {
|
||||
console.warn("Push notifications are not supported in this browser")
|
||||
return false
|
||||
}
|
||||
|
||||
this.vapidPublicKey = vapidPublicKey
|
||||
|
||||
try {
|
||||
// Register service worker
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register("/static/js/sw.js")
|
||||
console.log("Service Worker registered successfully")
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Service Worker registration failed:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermission() {
|
||||
if (!this.isSupported) {
|
||||
return "not-supported"
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission()
|
||||
console.log("Notification permission:", permission)
|
||||
return permission
|
||||
}
|
||||
|
||||
async subscribe() {
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
throw new Error("Service Worker not registered")
|
||||
}
|
||||
|
||||
try {
|
||||
const subscription = await this.serviceWorkerRegistration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
|
||||
})
|
||||
|
||||
console.log("Push subscription successful:", subscription)
|
||||
return subscription
|
||||
} catch (error) {
|
||||
console.error("Push subscription failed:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe() {
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription()
|
||||
if (subscription) {
|
||||
await subscription.unsubscribe()
|
||||
console.log("Push unsubscription successful")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error("Push unsubscription failed:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getSubscription() {
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.serviceWorkerRegistration.pushManager.getSubscription()
|
||||
} catch (error) {
|
||||
console.error("Failed to get subscription:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async sendSubscriptionToServer(subscription, deviceType = "web") {
|
||||
try {
|
||||
const response = await fetch("/api/communications/push-devices/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_token: JSON.stringify(subscription),
|
||||
device_type: deviceType,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
console.log("Subscription sent to server successfully")
|
||||
return true
|
||||
} else {
|
||||
console.error("Failed to send subscription to server:", response.statusText)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending subscription to server:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async removeSubscriptionFromServer(subscription) {
|
||||
try {
|
||||
const response = await fetch("/api/communications/push-devices/", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_token: JSON.stringify(subscription),
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
console.log("Subscription removed from server successfully")
|
||||
return true
|
||||
} else {
|
||||
console.error("Failed to remove subscription from server:", response.statusText)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error removing subscription from server:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
urlBase64ToUint8Array(base64String) {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
|
||||
|
||||
const rawData = window.atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray
|
||||
}
|
||||
|
||||
// Helper method to setup push notifications for authenticated users
|
||||
async setupPushNotifications(vapidPublicKey) {
|
||||
const initialized = await this.init(vapidPublicKey)
|
||||
if (!initialized) return false
|
||||
|
||||
const permission = await this.requestPermission()
|
||||
if (permission !== "granted") {
|
||||
console.log("Push notification permission denied")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const subscription = await this.subscribe()
|
||||
const sent = await this.sendSubscriptionToServer(subscription)
|
||||
return sent
|
||||
} catch (error) {
|
||||
console.error("Failed to setup push notifications:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
window.pushNotificationManager = new PushNotificationManager()
|
||||
|
||||
// Auto-setup for authenticated users (call this after user login)
|
||||
window.setupPushNotifications = async (vapidPublicKey) =>
|
||||
await window.pushNotificationManager.setupPushNotifications(vapidPublicKey)
|
||||
21
static/js/scripts.js
Normal file
21
static/js/scripts.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// Custom JavaScript for Django Unfold admin
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add confirmation for hard delete actions
|
||||
const hardDeleteButtons = document.querySelectorAll('[name="hard_delete"]');
|
||||
hardDeleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
if (!confirm('Are you sure you want to permanently delete this item? This action cannot be undone.')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-resize textareas
|
||||
const textareas = document.querySelectorAll('textarea');
|
||||
textareas.forEach(textarea => {
|
||||
textarea.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = this.scrollHeight + 'px';
|
||||
});
|
||||
});
|
||||
});
|
||||
95
static/js/sw.js
Normal file
95
static/js/sw.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// Service Worker for Push Notifications
|
||||
const CACHE_NAME = "cs-association-v1"
|
||||
const urlsToCache = [
|
||||
"/",
|
||||
"/static/css/styles.css",
|
||||
"/static/js/scripts.js",
|
||||
"/static/images/icon-192x192.png",
|
||||
"/static/images/icon-512x512.png",
|
||||
]
|
||||
|
||||
// Install event
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(urlsToCache)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// Fetch event
|
||||
self.addEventListener("fetch", (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
// Return cached version or fetch from network
|
||||
return response || fetch(event.request)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// Push event
|
||||
self.addEventListener("push", (event) => {
|
||||
if (event.data) {
|
||||
const data = event.data.json()
|
||||
const options = {
|
||||
body: data.body,
|
||||
icon: data.icon || "/static/images/icon-192x192.png",
|
||||
badge: data.badge || "/static/images/badge-72x72.png",
|
||||
data: data.data || {},
|
||||
actions: data.actions || [],
|
||||
dir: data.dir || "ltr",
|
||||
lang: data.lang || "en",
|
||||
requireInteraction: data.data && data.data.priority === "urgent",
|
||||
silent: false,
|
||||
tag: data.data ? `${data.data.type}-${data.data.announcement_id || data.data.event_id}` : "default",
|
||||
renotify: true,
|
||||
vibrate: data.data && data.data.priority === "urgent" ? [200, 100, 200] : [100, 50, 100],
|
||||
}
|
||||
|
||||
event.waitUntil(self.registration.showNotification(data.title, options))
|
||||
}
|
||||
})
|
||||
|
||||
// Notification click event
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close()
|
||||
|
||||
if (event.action === "dismiss") {
|
||||
return
|
||||
}
|
||||
|
||||
const data = event.notification.data
|
||||
let url = "/"
|
||||
|
||||
if (data && data.url) {
|
||||
url = data.url
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: "window" }).then((clientList) => {
|
||||
// Check if there's already a window/tab open with the target URL
|
||||
for (const client of clientList) {
|
||||
if (client.url === url && "focus" in client) {
|
||||
return client.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// If not, open a new window/tab
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// Background sync (for offline functionality)
|
||||
self.addEventListener("sync", (event) => {
|
||||
if (event.tag === "background-sync") {
|
||||
event.waitUntil(doBackgroundSync())
|
||||
}
|
||||
})
|
||||
|
||||
function doBackgroundSync() {
|
||||
// Implement background sync logic here
|
||||
return Promise.resolve()
|
||||
}
|
||||
Reference in New Issue
Block a user