profile changes
@@ -34,16 +34,16 @@ class TopListItemInline(admin.TabularInline):
|
|||||||
|
|
||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class CustomUserAdmin(UserAdmin):
|
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')
|
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',)
|
ordering = ('-date_joined',)
|
||||||
actions = ['activate_users', 'deactivate_users', 'ban_users', 'unban_users']
|
actions = ['activate_users', 'deactivate_users', 'ban_users', 'unban_users']
|
||||||
inlines = [UserProfileInline]
|
inlines = [UserProfileInline]
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
('Personal info', {'fields': ('first_name', 'last_name', 'email', 'pending_email')}),
|
('Personal info', {'fields': ('email', 'pending_email')}),
|
||||||
('Roles and Permissions', {
|
('Roles and Permissions', {
|
||||||
'fields': ('role', 'groups', 'user_permissions'),
|
'fields': ('role', 'groups', 'user_permissions'),
|
||||||
'description': 'Role determines group membership. Groups determine 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):
|
def get_status(self, obj):
|
||||||
if obj.is_banned:
|
if obj.is_banned:
|
||||||
return format_html('<span style="color: red;">Banned</span>')
|
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.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import random
|
import random
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from io import BytesIO
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
def generate_random_id(model_class, id_field):
|
def generate_random_id(model_class, id_field):
|
||||||
"""Generate a random ID starting at 4 digits, expanding to 5 if needed"""
|
"""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'
|
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(
|
role = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=Roles.choices,
|
choices=Roles.choices,
|
||||||
@@ -61,8 +63,9 @@ class User(AbstractUser):
|
|||||||
|
|
||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
"""Get the user's display name, falling back to username if not set"""
|
"""Get the user's display name, falling back to username if not set"""
|
||||||
if hasattr(self, 'profile') and self.profile.display_name:
|
profile = getattr(self, 'profile', None)
|
||||||
return self.profile.display_name
|
if profile and profile.display_name:
|
||||||
|
return profile.display_name
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@@ -106,10 +109,21 @@ class UserProfile(models.Model):
|
|||||||
flat_ride_credits = models.IntegerField(default=0)
|
flat_ride_credits = models.IntegerField(default=0)
|
||||||
water_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):
|
def save(self, *args, **kwargs):
|
||||||
# If no display name is set, use the username
|
# If no display name is set, use the username
|
||||||
if not self.display_name:
|
if not self.display_name:
|
||||||
self.display_name = self.user.username
|
self.display_name = self.user.username
|
||||||
|
|
||||||
if not self.profile_id:
|
if not self.profile_id:
|
||||||
self.profile_id = generate_random_id(UserProfile, 'profile_id')
|
self.profile_id = generate_random_id(UserProfile, 'profile_id')
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
@@ -155,7 +169,7 @@ class TopList(models.Model):
|
|||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.CASCADE,
|
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)
|
title = models.CharField(max_length=100)
|
||||||
category = models.CharField(
|
category = models.CharField(
|
||||||
@@ -170,7 +184,7 @@ class TopList(models.Model):
|
|||||||
ordering = ['-updated_at']
|
ordering = ['-updated_at']
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class TopListItem(models.Model):
|
||||||
top_list = models.ForeignKey(
|
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.db.models import Prefetch
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
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 reviews.models import Review
|
||||||
from email_service.services import EmailService
|
from email_service.services import EmailService
|
||||||
from allauth.account.views import LoginView, SignupView
|
from allauth.account.views import LoginView, SignupView
|
||||||
@@ -101,16 +101,8 @@ class ProfileView(DetailView):
|
|||||||
context['recent_reviews'] = reviews_queryset
|
context['recent_reviews'] = reviews_queryset
|
||||||
|
|
||||||
# Get user's top lists with optimized queries
|
# Get user's top lists with optimized queries
|
||||||
context['top_lists'] = user.top_lists.select_related(
|
top_lists_queryset = TopList.objects.filter(user=user).select_related('user', 'user__profile').prefetch_related('items')
|
||||||
'user',
|
context['top_lists'] = top_lists_queryset.order_by('-created_at')[:5]
|
||||||
'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]
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -128,8 +120,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
|||||||
if action == 'update_profile':
|
if action == 'update_profile':
|
||||||
# Handle profile updates
|
# Handle profile updates
|
||||||
user = request.user
|
user = request.user
|
||||||
user.first_name = request.POST.get('first_name', user.first_name)
|
user.profile.display_name = request.POST.get('display_name', user.profile.display_name)
|
||||||
user.last_name = request.POST.get('last_name', user.last_name)
|
|
||||||
|
|
||||||
if 'avatar' in request.FILES:
|
if 'avatar' in request.FILES:
|
||||||
user.profile.avatar = request.FILES['avatar']
|
user.profile.avatar = request.FILES['avatar']
|
||||||
@@ -150,7 +141,37 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
|||||||
return HttpResponseRedirect(reverse('account_login'))
|
return HttpResponseRedirect(reverse('account_login'))
|
||||||
else:
|
else:
|
||||||
messages.error(request, 'Current password is incorrect')
|
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)
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
def request_password_reset(request):
|
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 title %}{{ profile_user.username }}'s Profile - ThrillWiki{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4">
|
<div class="container px-4 mx-auto">
|
||||||
<!-- Profile Header -->
|
<!-- 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="p-6">
|
||||||
<div class="flex items-center">
|
<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 }}"
|
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">
|
<div class="ml-6">
|
||||||
<h1 class="text-2xl font-bold">
|
<h1 class="text-2xl font-bold">
|
||||||
{{ profile_user.profile.display_name|default:profile_user.username }}
|
{{ profile_user.profile.display_name|default:profile_user.username }}
|
||||||
@@ -31,14 +37,14 @@
|
|||||||
|
|
||||||
{% if profile_user.profile.bio %}
|
{% if profile_user.profile.bio %}
|
||||||
<div class="mt-6">
|
<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>
|
<p class="text-gray-600 dark:text-gray-400">{{ profile_user.profile.bio }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Social Links -->
|
<!-- Social Links -->
|
||||||
{% if profile_user.profile.twitter or profile_user.profile.instagram or profile_user.profile.youtube or profile_user.profile.discord %}
|
{% 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 %}
|
{% if profile_user.profile.twitter %}
|
||||||
<a href="{{ profile_user.profile.twitter }}" target="_blank" rel="noopener noreferrer"
|
<a href="{{ profile_user.profile.twitter }}" target="_blank" rel="noopener noreferrer"
|
||||||
class="text-gray-600 dark:text-gray-400 hover:text-blue-500">
|
class="text-gray-600 dark:text-gray-400 hover:text-blue-500">
|
||||||
@@ -68,27 +74,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics -->
|
<!-- 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">
|
||||||
<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-value">{{ profile_user.profile.coaster_credits }}</div>
|
||||||
<div class="stat-label">Coaster Credits</div>
|
<div class="stat-label">Coaster Credits</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<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-value">{{ profile_user.profile.dark_ride_credits }}</div>
|
||||||
<div class="stat-label">Dark Ride Credits</div>
|
<div class="stat-label">Dark Ride Credits</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<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-value">{{ profile_user.profile.flat_ride_credits }}</div>
|
||||||
<div class="stat-label">Flat Ride Credits</div>
|
<div class="stat-label">Flat Ride Credits</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<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-value">{{ profile_user.profile.water_ride_credits }}</div>
|
||||||
<div class="stat-label">Water Ride Credits</div>
|
<div class="stat-label">Water Ride Credits</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Activity -->
|
<!-- 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 -->
|
<!-- Recent Reviews -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -105,7 +111,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% for review in recent_reviews %}
|
{% for review in recent_reviews %}
|
||||||
<div class="mb-4 last:mb-0">
|
<div class="mb-4 last:mb-0">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium">{{ review.title }}</h3>
|
<h3 class="font-medium">{{ review.title }}</h3>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
@@ -113,7 +119,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<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>
|
<span>{{ review.rating }}/10</span>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
||||||