profile changes
@@ -34,16 +34,16 @@ class TopListItemInline(admin.TabularInline):
|
||||
|
||||
@admin.register(User)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
list_display = ('username', 'email', 'get_status', 'role', 'date_joined', 'last_login', 'get_credits')
|
||||
list_display = ('username', 'email', 'get_avatar', 'get_status', 'role', 'date_joined', 'last_login', 'get_credits')
|
||||
list_filter = ('is_active', 'is_staff', 'role', 'is_banned', 'groups', 'date_joined')
|
||||
search_fields = ('username', 'email', 'first_name', 'last_name')
|
||||
search_fields = ('username', 'email')
|
||||
ordering = ('-date_joined',)
|
||||
actions = ['activate_users', 'deactivate_users', 'ban_users', 'unban_users']
|
||||
inlines = [UserProfileInline]
|
||||
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
('Personal info', {'fields': ('first_name', 'last_name', 'email', 'pending_email')}),
|
||||
('Personal info', {'fields': ('email', 'pending_email')}),
|
||||
('Roles and Permissions', {
|
||||
'fields': ('role', 'groups', 'user_permissions'),
|
||||
'description': 'Role determines group membership. Groups determine permissions.',
|
||||
@@ -67,6 +67,12 @@ class CustomUserAdmin(UserAdmin):
|
||||
}),
|
||||
)
|
||||
|
||||
def get_avatar(self, obj):
|
||||
if obj.profile.avatar:
|
||||
return format_html('<img src="{}" width="30" height="30" style="border-radius:50%;" />', obj.profile.avatar.url)
|
||||
return format_html('<div style="width:30px; height:30px; border-radius:50%; background-color:#007bff; color:white; display:flex; align-items:center; justify-content:center;">{}</div>', obj.username[0].upper())
|
||||
get_avatar.short_description = 'Avatar'
|
||||
|
||||
def get_status(self, obj):
|
||||
if obj.is_banned:
|
||||
return format_html('<span style="color: red;">Banned</span>')
|
||||
|
||||
44
accounts/management/commands/generate_letter_avatars.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
def generate_avatar(letter):
|
||||
"""Generate an avatar for a given letter or number"""
|
||||
avatar_size = (100, 100)
|
||||
background_color = (0, 123, 255) # Blue background
|
||||
text_color = (255, 255, 255) # White text
|
||||
font_size = 100
|
||||
|
||||
# Create a blank image with background color
|
||||
image = Image.new('RGB', avatar_size, background_color)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Load a font
|
||||
font_path = "[AWS-SECRET-REMOVED]ans-Bold.ttf"
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
# Calculate text size and position using textbbox
|
||||
text_bbox = draw.textbbox((0, 0), letter, font=font)
|
||||
text_width, text_height = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
|
||||
text_position = ((avatar_size[0] - text_width) / 2, (avatar_size[1] - text_height) / 2)
|
||||
|
||||
# Draw the text on the image
|
||||
draw.text(text_position, letter, font=font, fill=text_color)
|
||||
|
||||
# Ensure the avatars directory exists
|
||||
avatar_dir = "avatars/letters"
|
||||
if not os.path.exists(avatar_dir):
|
||||
os.makedirs(avatar_dir)
|
||||
|
||||
# Save the image to the avatars directory
|
||||
avatar_path = os.path.join(avatar_dir, f"{letter}_avatar.png")
|
||||
image.save(avatar_path)
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Generate avatars for letters A-Z and numbers 0-9'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
characters = [chr(i) for i in range(65, 91)] + [str(i) for i in range(10)] # A-Z and 0-9
|
||||
for char in characters:
|
||||
generate_avatar(char)
|
||||
self.stdout.write(self.style.SUCCESS(f"Generated avatar for {char}"))
|
||||
11
accounts/management/commands/regenerate_avatars.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from accounts.models import UserProfile
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Regenerate default avatars for users without an uploaded avatar'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
profiles = UserProfile.objects.filter(avatar='')
|
||||
for profile in profiles:
|
||||
profile.save() # This will trigger the avatar generation logic in the save method
|
||||
self.stdout.write(self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}"))
|
||||
@@ -3,6 +3,10 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import random
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from io import BytesIO
|
||||
import base64
|
||||
import os
|
||||
|
||||
def generate_random_id(model_class, id_field):
|
||||
"""Generate a random ID starting at 4 digits, expanding to 5 if needed"""
|
||||
@@ -36,8 +40,6 @@ class User(AbstractUser):
|
||||
help_text='Unique identifier for this user that remains constant even if the username changes'
|
||||
)
|
||||
|
||||
first_name = models.CharField(_('first name'), max_length=150, default='')
|
||||
last_name = models.CharField(_('last name'), max_length=150, default='')
|
||||
role = models.CharField(
|
||||
max_length=10,
|
||||
choices=Roles.choices,
|
||||
@@ -61,8 +63,9 @@ class User(AbstractUser):
|
||||
|
||||
def get_display_name(self):
|
||||
"""Get the user's display name, falling back to username if not set"""
|
||||
if hasattr(self, 'profile') and self.profile.display_name:
|
||||
return self.profile.display_name
|
||||
profile = getattr(self, 'profile', None)
|
||||
if profile and profile.display_name:
|
||||
return profile.display_name
|
||||
return self.username
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -106,10 +109,21 @@ class UserProfile(models.Model):
|
||||
flat_ride_credits = models.IntegerField(default=0)
|
||||
water_ride_credits = models.IntegerField(default=0)
|
||||
|
||||
def get_avatar(self):
|
||||
"""Return the avatar URL or serve a pre-generated avatar based on the first letter of the username"""
|
||||
if self.avatar:
|
||||
return self.avatar.url
|
||||
first_letter = self.user.username[0].upper()
|
||||
avatar_path = f"avatars/letters/{first_letter}_avatar.png"
|
||||
if os.path.exists(avatar_path):
|
||||
return f"/{avatar_path}"
|
||||
return "/static/images/default-avatar.png"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# If no display name is set, use the username
|
||||
if not self.display_name:
|
||||
self.display_name = self.user.username
|
||||
|
||||
if not self.profile_id:
|
||||
self.profile_id = generate_random_id(UserProfile, 'profile_id')
|
||||
super().save(*args, **kwargs)
|
||||
@@ -155,7 +169,7 @@ class TopList(models.Model):
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='top_lists'
|
||||
related_name='top_lists' # Added related_name for User model access
|
||||
)
|
||||
title = models.CharField(max_length=100)
|
||||
category = models.CharField(
|
||||
@@ -170,7 +184,7 @@ class TopList(models.Model):
|
||||
ordering = ['-updated_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.get_display_name()}'s {self.get_category_display()} Top List: {self.title}"
|
||||
return f"{self.user.get_display_name()}'s {self.category} Top List: {self.title}"
|
||||
|
||||
class TopListItem(models.Model):
|
||||
top_list = models.ForeignKey(
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.db.models import Prefetch
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from accounts.models import User, PasswordReset
|
||||
from accounts.models import User, PasswordReset, TopList, EmailVerification
|
||||
from reviews.models import Review
|
||||
from email_service.services import EmailService
|
||||
from allauth.account.views import LoginView, SignupView
|
||||
@@ -101,16 +101,8 @@ class ProfileView(DetailView):
|
||||
context['recent_reviews'] = reviews_queryset
|
||||
|
||||
# Get user's top lists with optimized queries
|
||||
context['top_lists'] = user.top_lists.select_related(
|
||||
'user',
|
||||
'user__profile'
|
||||
).prefetch_related(
|
||||
Prefetch('items', queryset=(
|
||||
user.top_lists.through.objects.select_related(
|
||||
'content_type'
|
||||
).prefetch_related('content_object')
|
||||
))
|
||||
).order_by('-created_at')[:5]
|
||||
top_lists_queryset = TopList.objects.filter(user=user).select_related('user', 'user__profile').prefetch_related('items')
|
||||
context['top_lists'] = top_lists_queryset.order_by('-created_at')[:5]
|
||||
|
||||
return context
|
||||
|
||||
@@ -128,8 +120,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
if action == 'update_profile':
|
||||
# Handle profile updates
|
||||
user = request.user
|
||||
user.first_name = request.POST.get('first_name', user.first_name)
|
||||
user.last_name = request.POST.get('last_name', user.last_name)
|
||||
user.profile.display_name = request.POST.get('display_name', user.profile.display_name)
|
||||
|
||||
if 'avatar' in request.FILES:
|
||||
user.profile.avatar = request.FILES['avatar']
|
||||
@@ -151,6 +142,36 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
else:
|
||||
messages.error(request, 'Current password is incorrect')
|
||||
|
||||
elif action == 'change_email':
|
||||
# Handle email change with verification
|
||||
new_email = request.POST.get('new_email')
|
||||
if new_email:
|
||||
token = get_random_string(64)
|
||||
EmailVerification.objects.update_or_create(
|
||||
user=request.user,
|
||||
defaults={'token': token}
|
||||
)
|
||||
site = get_current_site(request)
|
||||
verification_url = reverse('verify_email', kwargs={'token': token})
|
||||
context = {
|
||||
'user': request.user,
|
||||
'verification_url': verification_url,
|
||||
'site_name': site.name,
|
||||
}
|
||||
email_html = render_to_string('accounts/email/verify_email.html', context)
|
||||
EmailService.send_email(
|
||||
to=new_email,
|
||||
subject='Verify your new email address',
|
||||
text='Click the link to verify your new email address',
|
||||
site=site,
|
||||
html=email_html
|
||||
)
|
||||
request.user.pending_email = new_email
|
||||
request.user.save()
|
||||
messages.success(request, 'Verification email sent to your new email address')
|
||||
else:
|
||||
messages.error(request, 'New email is required')
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def request_password_reset(request):
|
||||
|
||||
BIN
avatars/letters/0_avatar.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
avatars/letters/1_avatar.png
Normal file
|
After Width: | Height: | Size: 530 B |
BIN
avatars/letters/2_avatar.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
avatars/letters/3_avatar.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
avatars/letters/4_avatar.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
avatars/letters/5_avatar.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
avatars/letters/6_avatar.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
avatars/letters/7_avatar.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
avatars/letters/8_avatar.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
avatars/letters/9_avatar.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
avatars/letters/A_avatar.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
avatars/letters/B_avatar.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
avatars/letters/C_avatar.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
avatars/letters/D_avatar.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
avatars/letters/E_avatar.png
Normal file
|
After Width: | Height: | Size: 366 B |
BIN
avatars/letters/F_avatar.png
Normal file
|
After Width: | Height: | Size: 355 B |
BIN
avatars/letters/G_avatar.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
avatars/letters/H_avatar.png
Normal file
|
After Width: | Height: | Size: 362 B |
BIN
avatars/letters/I_avatar.png
Normal file
|
After Width: | Height: | Size: 317 B |
BIN
avatars/letters/J_avatar.png
Normal file
|
After Width: | Height: | Size: 486 B |
BIN
avatars/letters/K_avatar.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
avatars/letters/L_avatar.png
Normal file
|
After Width: | Height: | Size: 333 B |
BIN
avatars/letters/M_avatar.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
avatars/letters/N_avatar.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
avatars/letters/O_avatar.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
avatars/letters/P_avatar.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
avatars/letters/Q_avatar.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
avatars/letters/R_avatar.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
avatars/letters/S_avatar.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
avatars/letters/T_avatar.png
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
avatars/letters/U_avatar.png
Normal file
|
After Width: | Height: | Size: 910 B |
BIN
avatars/letters/V_avatar.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
avatars/letters/W_avatar.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
avatars/letters/X_avatar.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
avatars/letters/Y_avatar.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
avatars/letters/Z_avatar.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
media/avatars/loopy.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
@@ -4,14 +4,20 @@
|
||||
{% block title %}{{ profile_user.username }}'s Profile - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="container px-4 mx-auto">
|
||||
<!-- Profile Header -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center">
|
||||
<img src="{{ profile_user.profile.avatar.url|default:'/static/images/default-avatar.png' }}"
|
||||
{% if profile_user.profile.avatar %}
|
||||
<img src="{{ profile_user.profile.avatar.url }}"
|
||||
alt="{{ profile_user.username }}"
|
||||
class="w-24 h-24 rounded-full object-cover">
|
||||
class="object-cover w-24 h-24 rounded-full">
|
||||
{% else %}
|
||||
<div class="flex items-center justify-center w-24 h-24 text-white rounded-full bg-gradient-to-br from-primary to-secondary">
|
||||
{{ profile_user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ml-6">
|
||||
<h1 class="text-2xl font-bold">
|
||||
{{ profile_user.profile.display_name|default:profile_user.username }}
|
||||
@@ -31,14 +37,14 @@
|
||||
|
||||
{% if profile_user.profile.bio %}
|
||||
<div class="mt-6">
|
||||
<h2 class="text-lg font-semibold mb-2">About Me</h2>
|
||||
<h2 class="mb-2 text-lg font-semibold">About Me</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">{{ profile_user.profile.bio }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Social Links -->
|
||||
{% if profile_user.profile.twitter or profile_user.profile.instagram or profile_user.profile.youtube or profile_user.profile.discord %}
|
||||
<div class="mt-4 flex space-x-4">
|
||||
<div class="flex mt-4 space-x-4">
|
||||
{% if profile_user.profile.twitter %}
|
||||
<a href="{{ profile_user.profile.twitter }}" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-blue-500">
|
||||
@@ -68,27 +74,27 @@
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6">
|
||||
<div class="grid grid-cols-1 gap-4 mt-6 md:grid-cols-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-center card-body">
|
||||
<div class="stat-value">{{ profile_user.profile.coaster_credits }}</div>
|
||||
<div class="stat-label">Coaster Credits</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-center card-body">
|
||||
<div class="stat-value">{{ profile_user.profile.dark_ride_credits }}</div>
|
||||
<div class="stat-label">Dark Ride Credits</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-center card-body">
|
||||
<div class="stat-value">{{ profile_user.profile.flat_ride_credits }}</div>
|
||||
<div class="stat-label">Flat Ride Credits</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-center card-body">
|
||||
<div class="stat-value">{{ profile_user.profile.water_ride_credits }}</div>
|
||||
<div class="stat-label">Water Ride Credits</div>
|
||||
</div>
|
||||
@@ -96,7 +102,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
<div class="grid grid-cols-1 gap-6 mt-6 md:grid-cols-2">
|
||||
<!-- Recent Reviews -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -105,7 +111,7 @@
|
||||
<div class="card-body">
|
||||
{% for review in recent_reviews %}
|
||||
<div class="mb-4 last:mb-0">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 class="font-medium">{{ review.title }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
@@ -113,7 +119,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="text-yellow-400 mr-1">★</span>
|
||||
<span class="mr-1 text-yellow-400">★</span>
|
||||
<span>{{ review.rating }}/10</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
68
templates/accounts/settings.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Settings - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container px-4 mx-auto">
|
||||
<h1 class="mb-4 text-2xl font-bold">Settings</h1>
|
||||
|
||||
<div class="p-6 overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="update_profile">
|
||||
|
||||
<h2 class="mb-4 text-lg font-semibold">Update Profile</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="display_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Display Name</label>
|
||||
<input type="text" name="display_name" id="display_name" value="{{ user.profile.display_name }}" class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="avatar" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Avatar</label>
|
||||
<input type="file" name="avatar" id="avatar" class="block w-full mt-1 text-gray-900 dark:text-gray-300">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 text-white bg-blue-500 rounded-md">Update Profile</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-6 mt-6 overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="change_email">
|
||||
|
||||
<h2 class="mb-4 text-lg font-semibold">Change Email</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="new_email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">New Email</label>
|
||||
<input type="email" name="new_email" id="new_email" class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 text-white bg-blue-500 rounded-md">Change Email</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-6 mt-6 overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
|
||||
<h2 class="mb-4 text-lg font-semibold">Change Password</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="old_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Current Password</label>
|
||||
<input type="password" name="old_password" id="old_password" class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">New Password</label>
|
||||
<input type="password" name="new_password" id="new_password" class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 text-white bg-blue-500 rounded-md">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||