Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9aee95dfc | ||
|
|
79a273568c | ||
|
|
a08f77aa00 |
BIN
assets/pasted-20260217-172745-5275e704.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
assets/pasted-20260217-184853-b9782cb3.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
assets/pasted-20260217-185432-d5369dd4.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
assets/pasted-20260217-185935-a2ab29fc.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
assets/pasted-20260217-190126-64d72bd8.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
assets/pasted-20260217-190514-e29c5229.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'core',
|
'core',
|
||||||
|
'groups',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -180,3 +181,10 @@ if EMAIL_USE_SSL:
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# Media files (User uploads)
|
||||||
|
MEDIA_URL = 'media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
LOGOUT_REDIRECT_URL = 'onboarding'
|
||||||
|
LOGIN_REDIRECT_URL = 'home'
|
||||||
|
|||||||
@ -21,9 +21,11 @@ from django.conf.urls.static import static
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("groups/", include("groups.urls")),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
@ -1,3 +1,16 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import Intent, ValueTag, Profile
|
||||||
|
|
||||||
|
@admin.register(Intent)
|
||||||
|
class IntentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'icon')
|
||||||
|
|
||||||
|
@admin.register(ValueTag)
|
||||||
|
class ValueTagAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name',)
|
||||||
|
|
||||||
|
@admin.register(Profile)
|
||||||
|
class ProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'professional_headline', 'transition_status', 'location_city')
|
||||||
|
filter_horizontal = ('intents', 'value_tags')
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
|||||||
35
core/forms.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import Event, EventTag
|
||||||
|
|
||||||
|
class EventForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Event
|
||||||
|
fields = [
|
||||||
|
'title', 'description', 'start_datetime', 'end_datetime',
|
||||||
|
'timezone', 'location_name', 'location_address', 'city', 'state',
|
||||||
|
'is_online', 'online_url', 'visibility', 'group', 'capacity',
|
||||||
|
'cover_image', 'tags'
|
||||||
|
]
|
||||||
|
labels = {
|
||||||
|
'title': 'Session Title',
|
||||||
|
'description': 'Session Description',
|
||||||
|
'is_online': 'This is an online session',
|
||||||
|
'group': 'Squad',
|
||||||
|
}
|
||||||
|
widgets = {
|
||||||
|
'start_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
|
||||||
|
'end_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
|
||||||
|
'description': forms.Textarea(attrs={'rows': 4, 'class': 'form-control'}),
|
||||||
|
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'timezone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'location_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'location_address': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'city': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'state': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'online_url': forms.URLInput(attrs={'class': 'form-control'}),
|
||||||
|
'visibility': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'group': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'capacity': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
|
'cover_image': forms.FileInput(attrs={'class': 'form-control'}),
|
||||||
|
'tags': forms.SelectMultiple(attrs={'class': 'form-select'}),
|
||||||
|
}
|
||||||
46
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 16:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Intent',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('icon', models.CharField(blank=True, help_text='Bootstrap icon class name', max_length=50)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ValueTag',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Profile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('professional_headline', models.CharField(blank=True, max_length=255)),
|
||||||
|
('transition_status', models.CharField(choices=[('none', 'Stable'), ('post-divorce', 'Post-Divorce'), ('relocating', 'Relocating'), ('career-change', 'Career Change'), ('new-in-town', 'New in Town')], default='none', max_length=50)),
|
||||||
|
('bio', models.TextField(blank=True)),
|
||||||
|
('location_city', models.CharField(blank=True, max_length=100)),
|
||||||
|
('avatar_url', models.URLField(blank=True, null=True)),
|
||||||
|
('intents', models.ManyToManyField(blank=True, to='core.intent')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('value_tags', models.ManyToManyField(blank=True, to='core.valuetag')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
41
core/migrations/0002_profile_is_email_verified_and_more.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 16:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='is_email_verified',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='onboarding_completed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='organization_id',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='two_factor_enabled',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='user',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
55
core/migrations/0003_group_event_report.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 16:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_profile_is_email_verified_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('is_private', models.BooleanField(default=False)),
|
||||||
|
('organization_id', models.IntegerField(blank=True, null=True)),
|
||||||
|
('members', models.ManyToManyField(related_name='joined_groups', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('moderators', models.ManyToManyField(related_name='moderated_groups', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Event',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('start_time', models.DateTimeField()),
|
||||||
|
('end_time', models.DateTimeField()),
|
||||||
|
('location', models.CharField(max_length=255)),
|
||||||
|
('organization_id', models.IntegerField(blank=True, null=True)),
|
||||||
|
('attendees', models.ManyToManyField(related_name='attending_events', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='core.group')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Report',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('resolved', models.BooleanField(default=False)),
|
||||||
|
('organization_id', models.IntegerField(blank=True, null=True)),
|
||||||
|
('reported_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reports_received', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports_made', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
30
core/migrations/0004_message.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 16:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_group_event_report'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('body', models.TextField()),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('is_read', models.BooleanField(default=False)),
|
||||||
|
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['timestamp'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_message'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='profile',
|
||||||
|
name='avatar_url',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='avatar',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='avatars/'),
|
||||||
|
),
|
||||||
|
]
|
||||||
65
core/migrations/0006_post_comment_hiddenpost_reaction.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:06
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_remove_profile_avatar_url_profile_avatar'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Post',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='posts/')),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-timestamp'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Comment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.post')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['timestamp'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HiddenPost',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hidden_posts', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.post')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('user', 'post')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Reaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('reaction_type', models.CharField(choices=[('heart', 'Heart'), ('like', 'Like'), ('smile', 'Smile')], default='heart', max_length=20)),
|
||||||
|
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='core.post')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('post', 'user', 'reaction_type')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_post_comment_hiddenpost_reaction'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='post',
|
||||||
|
name='post_type',
|
||||||
|
field=models.CharField(choices=[('reflection', 'Reflection'), ('looking_for', 'Looking For'), ('offering', 'Offering'), ('event_invite', 'Event Invite'), ('progress_update', 'Progress Update'), ('skill_share', 'Skill Share')], default='reflection', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='accountability_streak',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Connection',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections1', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('user2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections2', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('user1', 'user2')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
28
core/migrations/0008_follow.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0007_post_post_type_profile_accountability_streak_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Follow',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('followed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_relations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_relations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('follower', 'followed')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:34
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0008_follow'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='recipient',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Thread',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('participants', models.ManyToManyField(related_name='threads', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='thread',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='core.thread'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Block',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('blocked', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks_received', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('blocker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks_given', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('blocker', 'blocked')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ConnectionRequest',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('declined', 'Declined'), ('canceled', 'Canceled'), ('blocked', 'Blocked')], default='pending', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('responded_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests_sent', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests_received', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('from_user', 'to_user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Like',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes_given', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes_received', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('from_user', 'to_user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Match',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('status', models.CharField(choices=[('active', 'Active'), ('archived', 'Archived')], default='active', max_length=20)),
|
||||||
|
('user_a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches_a', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('user_b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches_b', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('user_a', 'user_b')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
157
core/migrations/0010_eventtag_remove_event_attendees_and_more.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0009_alter_message_recipient_thread_message_thread_block_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventTag',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50, unique=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='attendees',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='end_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='location',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='organization_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='organizer',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_time',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='capacity',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='city',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='cover_image',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='events/'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='end_datetime',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='is_online',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='location_address',
|
||||||
|
field=models.CharField(blank=True, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='location_name',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='online_url',
|
||||||
|
field=models.URLField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_datetime',
|
||||||
|
field=models.DateTimeField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='timezone',
|
||||||
|
field=models.CharField(default='UTC', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('public', 'Public (Members Only)'), ('group', 'Group Only'), ('invite', 'Invite Only')], default='public', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='group',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_events', to='core.group'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventInvite',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('declined', 'Declined')], default='pending', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='core.event')),
|
||||||
|
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites_sent', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites_received', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='tags',
|
||||||
|
field=models.ManyToManyField(blank=True, to='core.eventtag'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RSVP',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('going', 'Going'), ('maybe', 'Maybe'), ('not_going', 'Not Going')], max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rsvps', to='core.event')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rsvps', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('event', 'user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 17:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0010_eventtag_remove_event_attendees_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='location_name',
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_datetime',
|
||||||
|
field=models.DateTimeField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 18:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0011_alter_event_creator_alter_event_location_name_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Game',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True)),
|
||||||
|
('slug', models.SlugField(max_length=255, unique=True)),
|
||||||
|
('genre', models.CharField(choices=[('fps', 'FPS'), ('moba', 'MOBA'), ('battle_royale', 'Battle Royale'), ('sports', 'Sports'), ('mmo', 'MMO'), ('fighting', 'Fighting'), ('rpg', 'RPG'), ('strategy', 'Strategy'), ('other', 'Other')], max_length=50)),
|
||||||
|
('team_size', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('has_roles', models.BooleanField(default=False)),
|
||||||
|
('roles_json', models.JSONField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='gamer_tag',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='platform',
|
||||||
|
field=models.CharField(blank=True, choices=[('pc', 'PC'), ('playstation', 'PlayStation'), ('xbox', 'Xbox'), ('nintendo', 'Nintendo Switch'), ('mobile', 'Mobile')], max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='preferred_role',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='rank',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='primary_game',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_players', to='core.game'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='secondary_games',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='secondary_players', to='core.game'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 18:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0012_game_profile_gamer_tag_profile_platform_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='gamer_tag',
|
||||||
|
field=models.CharField(max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='platform',
|
||||||
|
field=models.CharField(choices=[('pc', 'PC'), ('playstation', 'PlayStation'), ('xbox', 'Xbox'), ('nintendo', 'Nintendo Switch'), ('mobile', 'Mobile')], max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='primary_game',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_players', to='core.game'),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_message.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0008_follow.cpython-311.pyc
Normal file
363
core/models.py
@ -1,3 +1,364 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
# Create your models here.
|
class Intent(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
icon = models.CharField(max_length=50, blank=True, help_text="Bootstrap icon class name")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
class ValueTag(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
class Game(models.Model):
|
||||||
|
GENRE_CHOICES = [
|
||||||
|
('fps', 'FPS'),
|
||||||
|
('moba', 'MOBA'),
|
||||||
|
('battle_royale', 'Battle Royale'),
|
||||||
|
('sports', 'Sports'),
|
||||||
|
('mmo', 'MMO'),
|
||||||
|
('fighting', 'Fighting'),
|
||||||
|
('rpg', 'RPG'),
|
||||||
|
('strategy', 'Strategy'),
|
||||||
|
('other', 'Other'),
|
||||||
|
]
|
||||||
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
slug = models.SlugField(max_length=255, unique=True)
|
||||||
|
genre = models.CharField(max_length=50, choices=GENRE_CHOICES)
|
||||||
|
team_size = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
has_roles = models.BooleanField(default=False)
|
||||||
|
roles_json = models.JSONField(null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
class Profile(models.Model):
|
||||||
|
PLATFORM_CHOICES = [
|
||||||
|
('pc', 'PC'),
|
||||||
|
('playstation', 'PlayStation'),
|
||||||
|
('xbox', 'Xbox'),
|
||||||
|
('nintendo', 'Nintendo Switch'),
|
||||||
|
('mobile', 'Mobile'),
|
||||||
|
]
|
||||||
|
TRANSITION_CHOICES = [
|
||||||
|
('none', 'Stable'),
|
||||||
|
('post-divorce', 'Post-Divorce'),
|
||||||
|
('relocating', 'Relocating'),
|
||||||
|
('career-change', 'Career Change'),
|
||||||
|
('new-in-town', 'New in Town'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||||
|
professional_headline = models.CharField(max_length=255, blank=True)
|
||||||
|
transition_status = models.CharField(max_length=50, choices=TRANSITION_CHOICES, default='none')
|
||||||
|
bio = models.TextField(blank=True)
|
||||||
|
location_city = models.CharField(max_length=100, blank=True)
|
||||||
|
intents = models.ManyToManyField(Intent, blank=True)
|
||||||
|
value_tags = models.ManyToManyField(ValueTag, blank=True)
|
||||||
|
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||||
|
|
||||||
|
# Gaming Fields
|
||||||
|
primary_game = models.ForeignKey(Game, on_delete=models.SET_NULL, null=True, blank=False, related_name='primary_players')
|
||||||
|
secondary_games = models.ManyToManyField(Game, blank=True, related_name='secondary_players')
|
||||||
|
platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES, blank=False)
|
||||||
|
gamer_tag = models.CharField(max_length=100, blank=False)
|
||||||
|
rank = models.CharField(max_length=100, blank=True)
|
||||||
|
preferred_role = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
# Momentum & Engagement
|
||||||
|
accountability_streak = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
# Auth & Security
|
||||||
|
is_email_verified = models.BooleanField(default=False)
|
||||||
|
two_factor_enabled = models.BooleanField(default=False)
|
||||||
|
onboarding_completed = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# Multitenancy (Future Proofing)
|
||||||
|
organization_id = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_avatar_url(self):
|
||||||
|
if self.avatar and hasattr(self.avatar, 'url'):
|
||||||
|
return self.avatar.url
|
||||||
|
return f"https://i.pravatar.cc/150?u={self.user.username}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection_count(self):
|
||||||
|
return Match.objects.filter(models.Q(user_a=self.user) | models.Q(user_b=self.user)).count()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def following_count(self):
|
||||||
|
return Follow.objects.filter(follower=self.user).count()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def followers_count(self):
|
||||||
|
return Follow.objects.filter(followed=self.user).count()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def events_attended_count(self):
|
||||||
|
return self.user.rsvps.filter(status='going').count()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_completion_percentage(self):
|
||||||
|
steps = 0
|
||||||
|
total_steps = 6
|
||||||
|
if self.gamer_tag: steps += 1
|
||||||
|
if self.bio: steps += 1
|
||||||
|
if self.location_city: steps += 1
|
||||||
|
if self.intents.exists(): steps += 1
|
||||||
|
if self.avatar: steps += 1
|
||||||
|
if self.primary_game: steps += 1
|
||||||
|
return int((steps / total_steps) * 100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}'s Profile"
|
||||||
|
|
||||||
|
class Connection(models.Model):
|
||||||
|
user1 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='connections1')
|
||||||
|
user2 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='connections2')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('user1', 'user2')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user1.username} <-> {self.user2.username}"
|
||||||
|
|
||||||
|
class Follow(models.Model):
|
||||||
|
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name='following_relations')
|
||||||
|
followed = models.ForeignKey(User, on_delete=models.CASCADE, related_name='follower_relations')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('follower', 'followed')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.follower.username} follows {self.followed.username}"
|
||||||
|
|
||||||
|
class Group(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField()
|
||||||
|
is_private = models.BooleanField(default=False)
|
||||||
|
moderators = models.ManyToManyField(User, related_name='moderated_groups')
|
||||||
|
members = models.ManyToManyField(User, related_name='joined_groups')
|
||||||
|
organization_id = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
class EventTag(models.Model):
|
||||||
|
name = models.CharField(max_length=50, unique=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
class Event(models.Model):
|
||||||
|
VISIBILITY_CHOICES = [
|
||||||
|
('public', 'Public (Members Only)'),
|
||||||
|
('group', 'Group Only'),
|
||||||
|
('invite', 'Invite Only'),
|
||||||
|
]
|
||||||
|
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_events')
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
description = models.TextField()
|
||||||
|
start_datetime = models.DateTimeField()
|
||||||
|
end_datetime = models.DateTimeField(null=True, blank=True)
|
||||||
|
timezone = models.CharField(max_length=50, default='UTC')
|
||||||
|
location_name = models.CharField(max_length=255)
|
||||||
|
location_address = models.CharField(max_length=255, blank=True)
|
||||||
|
city = models.CharField(max_length=100, blank=True)
|
||||||
|
state = models.CharField(max_length=100, blank=True)
|
||||||
|
is_online = models.BooleanField(default=False)
|
||||||
|
online_url = models.URLField(blank=True)
|
||||||
|
visibility = models.CharField(max_length=20, choices=VISIBILITY_CHOICES, default='public')
|
||||||
|
group = models.ForeignKey(Group, on_delete=models.SET_NULL, null=True, blank=True, related_name='group_events')
|
||||||
|
capacity = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
cover_image = models.ImageField(upload_to='events/', blank=True, null=True)
|
||||||
|
tags = models.ManyToManyField(EventTag, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, null=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.title)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rsvp_count(self):
|
||||||
|
return self.rsvps.filter(status='going').count()
|
||||||
|
|
||||||
|
class RSVP(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('going', 'Going'),
|
||||||
|
('maybe', 'Maybe'),
|
||||||
|
('not_going', 'Not Going'),
|
||||||
|
]
|
||||||
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='rsvps')
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='rsvps')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('event', 'user')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} - {self.event.title} ({self.status})"
|
||||||
|
|
||||||
|
class EventInvite(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', 'Pending'),
|
||||||
|
('accepted', 'Accepted'),
|
||||||
|
('declined', 'Declined'),
|
||||||
|
]
|
||||||
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='invites')
|
||||||
|
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='invites_sent')
|
||||||
|
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='invites_received')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Invite: {self.event.title} to {self.to_user.username}"
|
||||||
|
|
||||||
|
class Report(models.Model):
|
||||||
|
reporter = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_made')
|
||||||
|
reported_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_received', null=True, blank=True)
|
||||||
|
content = models.TextField()
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
resolved = models.BooleanField(default=False)
|
||||||
|
organization_id = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Report by {self.reporter.username} at {self.timestamp}"
|
||||||
|
|
||||||
|
class Thread(models.Model):
|
||||||
|
participants = models.ManyToManyField(User, related_name='threads')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Thread with {self.participants.count()} participants"
|
||||||
|
|
||||||
|
class Message(models.Model):
|
||||||
|
thread = models.ForeignKey(Thread, on_delete=models.CASCADE, related_name='messages', null=True, blank=True)
|
||||||
|
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
|
||||||
|
recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages', null=True, blank=True)
|
||||||
|
body = models.TextField()
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_read = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['timestamp']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"From {self.sender.username} at {self.timestamp}"
|
||||||
|
|
||||||
|
class Like(models.Model):
|
||||||
|
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='likes_given')
|
||||||
|
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='likes_received')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('from_user', 'to_user')
|
||||||
|
|
||||||
|
class ConnectionRequest(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', 'Pending'),
|
||||||
|
('accepted', 'Accepted'),
|
||||||
|
('declined', 'Declined'),
|
||||||
|
('canceled', 'Canceled'),
|
||||||
|
('blocked', 'Blocked'),
|
||||||
|
]
|
||||||
|
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='requests_sent')
|
||||||
|
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='requests_received')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
responded_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('from_user', 'to_user')
|
||||||
|
|
||||||
|
class Match(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('active', 'Active'),
|
||||||
|
('archived', 'Archived'),
|
||||||
|
]
|
||||||
|
user_a = models.ForeignKey(User, on_delete=models.CASCADE, related_name='matches_a')
|
||||||
|
user_b = models.ForeignKey(User, on_delete=models.CASCADE, related_name='matches_b')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('user_a', 'user_b')
|
||||||
|
|
||||||
|
class Block(models.Model):
|
||||||
|
blocker = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blocks_given')
|
||||||
|
blocked = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blocks_received')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('blocker', 'blocked')
|
||||||
|
|
||||||
|
class Post(models.Model):
|
||||||
|
POST_TYPE_CHOICES = [
|
||||||
|
('reflection', 'Reflection'),
|
||||||
|
('looking_for', 'Looking For'),
|
||||||
|
('offering', 'Offering'),
|
||||||
|
('event_invite', 'Event Invite'),
|
||||||
|
('progress_update', 'Progress Update'),
|
||||||
|
('skill_share', 'Skill Share'),
|
||||||
|
]
|
||||||
|
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
|
||||||
|
content = models.TextField()
|
||||||
|
image = models.ImageField(upload_to='posts/', blank=True, null=True)
|
||||||
|
post_type = models.CharField(max_length=20, choices=POST_TYPE_CHOICES, default='reflection')
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-timestamp']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Post by {self.author.username} at {self.timestamp}"
|
||||||
|
|
||||||
|
def user_has_reacted(self, user, reaction_type='heart'):
|
||||||
|
if user.is_authenticated:
|
||||||
|
return self.reactions.filter(user=user, reaction_type=reaction_type).exists()
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
|
||||||
|
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
content = models.TextField()
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['timestamp']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Comment by {self.author.username} on {self.post}"
|
||||||
|
|
||||||
|
class Reaction(models.Model):
|
||||||
|
REACTION_CHOICES = [
|
||||||
|
('heart', 'Heart'),
|
||||||
|
('like', 'Like'),
|
||||||
|
('smile', 'Smile'),
|
||||||
|
]
|
||||||
|
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='reactions')
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
reaction_type = models.CharField(max_length=20, choices=REACTION_CHOICES, default='heart')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('post', 'user', 'reaction_type')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} {self.reaction_type}ed {self.post}"
|
||||||
|
|
||||||
|
class HiddenPost(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hidden_posts')
|
||||||
|
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('user', 'post')
|
||||||
|
|||||||
BIN
core/static/core/images/logo.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
@ -3,23 +3,116 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Roster{% endblock %}</title>
|
||||||
{% if project_description %}
|
{% if project_description %}
|
||||||
<meta name="description" content="{{ project_description }}">
|
<meta name="description" content="{{ project_description }}">
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
|
||||||
{% endif %}
|
|
||||||
{% if project_image_url %}
|
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<!-- Bootstrap 5 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<nav class="navbar navbar-expand-lg sticky-top py-2">
|
||||||
|
<div class="container-fluid px-lg-5">
|
||||||
|
<a class="navbar-brand brand-font d-flex align-items-center" href="/">
|
||||||
|
<img src="{% static 'core/images/logo.png' %}?v={{ deployment_timestamp }}" alt="Roster Logo" style="height: 48px; filter: drop-shadow(0 0 10px rgba(0, 180, 219, 0.3));">
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler border-0 text-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<i class="bi bi-list fs-1"></i>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto align-items-center">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="{% url 'home' %}">Explore</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="{% url 'about' %}">About</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3 {% if 'matches' in request.path %}active{% endif %}" href="{% url 'matches' %}">My Roster</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3 {% if 'events' in request.path %}active{% endif %}" href="{% url 'events' %}">Sessions</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3 {% if 'groups' in request.path %}active{% endif %}" href="{% url 'groups:hub' %}">Squads</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="{% url 'inbox' %}">Messages</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="{% url 'my_profile' %}">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="{% url 'settings' %}">Settings</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="/admin/">Admin</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<form action="{% url 'logout' %}" method="post" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-outline-cg ms-lg-3">Log Out</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3" href="{% url 'login' %}">Log In</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="btn btn-neon ms-lg-3" href="{% url 'signup' %}">Join Roster</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Mobile Bottom Nav -->
|
||||||
|
<div class="bottom-nav d-lg-none">
|
||||||
|
<a href="{% url 'home' %}" class="bottom-nav-item {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
|
||||||
|
<i class="bi bi-compass"></i>
|
||||||
|
Explore
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'matches' %}" class="bottom-nav-item {% if 'matches' in request.path %}active{% endif %}">
|
||||||
|
<i class="bi bi-shield-check"></i>
|
||||||
|
My Roster
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'inbox' %}" class="bottom-nav-item {% if request.resolver_match.url_name == 'inbox' or request.resolver_match.url_name == 'chat_detail' %}active{% endif %}">
|
||||||
|
<i class="bi bi-chat-left-text"></i>
|
||||||
|
Messages
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'events' %}" class="bottom-nav-item {% if request.resolver_match.url_name == 'events' %}active{% endif %}">
|
||||||
|
<i class="bi bi-lightning-charge"></i>
|
||||||
|
Sessions
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'groups:hub' %}" class="bottom-nav-item {% if 'groups' in request.path %}active{% endif %}">
|
||||||
|
<i class="bi bi-grid-1x2"></i>
|
||||||
|
Squads
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'my_profile' %}" class="bottom-nav-item {% if request.resolver_match.url_name == 'my_profile' or request.resolver_match.url_name == 'profile_detail' or request.resolver_match.url_name == 'edit_profile' %}active{% endif %}">
|
||||||
|
<i class="bi bi-person-badge"></i>
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 JS Bundle -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
49
core/templates/core/about.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}About - Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light-cg">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center text-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<h1 class="brand-font display-4 mb-4">Our Mission</h1>
|
||||||
|
<p class="lead text-slate fs-4">Roster is a platform for intentional beauty connections, ritual sharing, and community growth.</p>
|
||||||
|
<p class="text-muted">Not a dating app — a high-trust social connection ecosystem centered on shared values, life goals, activities, and authentic engagement.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm p-4">
|
||||||
|
<h3 class="brand-font h4">High Trust</h3>
|
||||||
|
<p>Verified members and moderated communities ensure a safe and respectful environment.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm p-4">
|
||||||
|
<h3 class="brand-font h4">Value Centered</h3>
|
||||||
|
<p>Match based on shared values like growth, community, and adventure.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm p-4">
|
||||||
|
<h3 class="brand-font h4">Intent Driven</h3>
|
||||||
|
<p>Clearly state what you're looking for: networking, friendship, or activity partners.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-5 bg-sand">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h2 class="brand-font mb-4">Ready to find your community?</h2>
|
||||||
|
<a href="{% url 'signup' %}" class="btn btn-primary-cg btn-lg">Join Roster</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
54
core/templates/core/chat.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Chat with {{ partner.first_name }} | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm border-0 rounded-3">
|
||||||
|
<div class="card-header bg-white p-3 border-bottom d-flex align-items-center">
|
||||||
|
<a href="{% url 'inbox' %}" class="btn btn-link text-dark p-0 me-3"><i class="bi bi-chevron-left h4 mb-0"></i></a>
|
||||||
|
<img src="{{ partner.profile.avatar_url|default:'https://ui-avatars.com/api/?name='|add:partner.first_name }}" class="rounded-circle me-2" width="40" height="40" alt="{{ partner.username }}">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0 fw-bold">{{ partner.get_full_name|default:partner.username }}</h6>
|
||||||
|
<small class="text-success">Active now</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4" style="height: 450px; overflow-y: auto;" id="message-container">
|
||||||
|
{% for message in chat_messages %}
|
||||||
|
<div class="d-flex mb-3 {% if message.sender == request.user %}justify-content-end{% endif %}">
|
||||||
|
<div class="p-3 rounded-3 {% if message.sender == request.user %}bg-primary text-white{% else %}bg-light text-dark{% endif %}" style="max-width: 75%;">
|
||||||
|
<p class="mb-1">{{ message.body }}</p>
|
||||||
|
<small class="{% if message.sender == request.user %}text-white-50{% else %}text-muted{% endif %} d-block text-end" style="font-size: 0.7rem;">
|
||||||
|
{{ message.timestamp|date:"H:i" }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<p class="text-muted">No messages yet. Say hello to {{ partner.first_name }}!</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer bg-white p-3 border-top">
|
||||||
|
<form method="POST" class="d-flex gap-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" name="body" class="form-control rounded-pill border-light bg-light px-3" placeholder="Type a message..." required autocomplete="off">
|
||||||
|
<button type="submit" class="btn btn-primary-cg rounded-circle p-2 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||||
|
<i class="bi bi-send-fill text-white"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Scroll to bottom of message container
|
||||||
|
const container = document.getElementById('message-container');
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
104
core/templates/core/edit_profile.html
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h2 class="fw-bold mb-4">Edit Profile</h2>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="position-relative d-inline-block">
|
||||||
|
<img src="{{ profile.get_avatar_url }}"
|
||||||
|
class="rounded-circle border border-3 border-light shadow-sm"
|
||||||
|
style="width: 120px; height: 120px; object-fit: cover;"
|
||||||
|
alt="Current Avatar"
|
||||||
|
id="avatar-preview">
|
||||||
|
<label for="avatar-input" class="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle shadow-sm" style="width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<i class="bi bi-camera"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="form-label small text-muted">Click the camera icon to upload a new picture</label>
|
||||||
|
<input type="file" name="avatar" id="avatar-input" class="d-none" accept="image/*" onchange="previewImage(this)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="headline" class="form-label fw-semibold">Gamer Headline</label>
|
||||||
|
<input type="text" class="form-control" id="headline" name="headline" value="{{ profile.professional_headline }}" placeholder="e.g. Competitive Apex Player | Support Main">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="primary_game" class="form-label fw-semibold">Primary Game</label>
|
||||||
|
<select name="primary_game" id="primary_game" class="form-select">
|
||||||
|
<option value="">Select a game</option>
|
||||||
|
{% for game in games %}
|
||||||
|
<option value="{{ game.id }}" {% if profile.primary_game_id == game.id %}selected{% endif %}>{{ game.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="platform" class="form-label fw-semibold">Platform</label>
|
||||||
|
<select name="platform" id="platform" class="form-select">
|
||||||
|
<option value="">Select a platform</option>
|
||||||
|
{% for code, label in platforms %}
|
||||||
|
<option value="{{ code }}" {% if profile.platform == code %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="gamer_tag" class="form-label fw-semibold">Gamer Tag</label>
|
||||||
|
<input type="text" class="form-control" id="gamer_tag" name="gamer_tag" value="{{ profile.gamer_tag }}" placeholder="e.g. Ninja#1234">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="rank" class="form-label fw-semibold">Current Rank</label>
|
||||||
|
<input type="text" class="form-control" id="rank" name="rank" value="{{ profile.rank }}" placeholder="e.g. Diamond II">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="preferred_role" class="form-label fw-semibold">Preferred Role</label>
|
||||||
|
<input type="text" class="form-control" id="preferred_role" name="preferred_role" value="{{ profile.preferred_role }}" placeholder="e.g. Tank / Support">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="location" class="form-label fw-semibold">Location (City)</label>
|
||||||
|
<input type="text" class="form-control" id="location" name="location" value="{{ profile.location_city }}" placeholder="e.g. Austin, TX">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="bio" class="form-label fw-semibold">Gamer Bio</label>
|
||||||
|
<textarea class="form-control" id="bio" name="bio" rows="4" placeholder="Tell others about your playstyle, schedule, and what kind of teammates you're looking for...">{{ profile.bio }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary py-2 fw-bold text-uppercase">Save Changes</button>
|
||||||
|
<a href="{% url 'my_profile' %}" class="btn btn-outline-secondary py-2 fw-bold text-uppercase">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function previewImage(input) {
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
document.getElementById('avatar-preview').src = e.target.result;
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
29
core/templates/core/event_confirm_delete.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Delete Event | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4 text-center">
|
||||||
|
<div class="bg-danger-subtle rounded-circle d-inline-flex p-3 mb-3">
|
||||||
|
<i class="bi bi-exclamation-triangle text-danger h3 mb-0"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="fw-bold h4 mb-3">Delete Event?</h2>
|
||||||
|
<p class="text-muted small mb-4">Are you sure you want to delete "<strong>{{ event.title }}</strong>"? This action cannot be undone and all RSVPs will be lost.</p>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-danger rounded-pill">Yes, Delete Event</button>
|
||||||
|
<a href="{% url 'event_detail' event.id %}" class="btn btn-light rounded-pill">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
149
core/templates/core/event_detail.html
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ event.title }} | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Event Header -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 overflow-hidden mb-4">
|
||||||
|
{% if event.cover_image %}
|
||||||
|
<img src="{{ event.cover_image.url }}" class="w-100" style="max-height: 400px; object-fit: cover;">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="badge rounded-pill bg-dark extra-small mb-2">{{ event.get_visibility_display }}</span>
|
||||||
|
<h1 class="h2 fw-bold mb-1">{{ event.title }}</h1>
|
||||||
|
<p class="text-muted"><i class="bi bi-geo-alt me-1"></i> {{ event.location_name }}{% if event.city %}, {{ event.city }}{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
{% if event.creator == user %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-light rounded-circle" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-three-dots-vertical"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
|
||||||
|
<li><a class="dropdown-item small" href="{% url 'event_edit' event.id %}"><i class="bi bi-pencil me-2"></i> Edit Session</a></li>
|
||||||
|
...
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="fw-bold h6 mb-3">About this session</h5>
|
||||||
|
<div class="small text-secondary" style="white-space: pre-line;">
|
||||||
|
{{ event.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.is_online and event.online_url %}
|
||||||
|
<div class="alert alert-primary border-0 rounded-4 d-flex align-items-center gap-3">
|
||||||
|
<i class="bi bi-laptop h4 mb-0"></i>
|
||||||
|
<div>
|
||||||
|
<p class="small fw-bold mb-0">This is an online session</p>
|
||||||
|
<a href="{{ event.online_url }}" class="extra-small text-decoration-none" target="_blank">{{ event.online_url }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="d-flex align-items-center gap-3 p-3 bg-light rounded-3">
|
||||||
|
<div class="bg-white rounded-3 p-2 shadow-sm text-center" style="min-width: 60px;">
|
||||||
|
<span class="d-block extra-small text-uppercase fw-bold text-primary">{{ event.start_datetime|date:"M" }}</span>
|
||||||
|
<span class="h4 fw-bold mb-0">{{ event.start_datetime|date:"d" }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="small fw-bold mb-0">{{ event.start_datetime|date:"l, P" }}</p>
|
||||||
|
<p class="extra-small text-muted mb-0">{{ event.timezone }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="d-flex align-items-center gap-3 p-3 bg-light rounded-3 h-100">
|
||||||
|
<div class="bg-white rounded-3 p-2 shadow-sm d-flex align-items-center justify-content-center" style="min-width: 60px; height: 60px;">
|
||||||
|
<i class="bi bi-people h4 mb-0 text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="small fw-bold mb-0">{{ going_count }} Going</p>
|
||||||
|
<p class="extra-small text-muted mb-0">{% if event.capacity %}{{ event.capacity }} capacity{% else %}Unlimited space{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="fw-bold h6 mb-3">About this event</h5>
|
||||||
|
<div class="small text-secondary" style="white-space: pre-line;">
|
||||||
|
{{ event.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.is_online and event.online_url %}
|
||||||
|
<div class="alert alert-primary border-0 rounded-4 d-flex align-items-center gap-3">
|
||||||
|
<i class="bi bi-laptop h4 mb-0"></i>
|
||||||
|
<div>
|
||||||
|
<p class="small fw-bold mb-0">This is an online event</p>
|
||||||
|
<a href="{{ event.online_url }}" class="extra-small text-decoration-none" target="_blank">{{ event.online_url }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- RSVP Widget -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold h6 mb-3">RSVP</h5>
|
||||||
|
{% if user_rsvp %}
|
||||||
|
<div class="alert alert-{% if user_rsvp.status == 'going' %}success{% elif user_rsvp.status == 'maybe' %}info{% else %}secondary{% endif %} border-0 small mb-3">
|
||||||
|
You are marked as: <strong>{{ user_rsvp.get_status_display }}</strong>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="{% url 'event_rsvp' event.id %}" method="POST" class="d-grid gap-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="status" value="going" class="btn {% if user_rsvp.status == 'going' %}btn-primary-cg{% else %}btn-outline-primary-cg{% endif %} rounded-pill btn-sm">Going</button>
|
||||||
|
<button type="submit" name="status" value="maybe" class="btn {% if user_rsvp.status == 'maybe' %}btn-primary-cg{% else %}btn-outline-primary-cg{% endif %} rounded-pill btn-sm">Maybe</button>
|
||||||
|
<button type="submit" name="status" value="not_going" class="btn {% if user_rsvp.status == 'not_going' %}btn-primary-cg{% else %}btn-outline-primary-cg{% endif %} rounded-pill btn-sm">Not Going</button>
|
||||||
|
{% if user_rsvp %}
|
||||||
|
<button type="submit" name="status" value="cancel" class="btn btn-link text-muted extra-small text-decoration-none">Cancel RSVP</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Organizer Widget -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4 text-center">
|
||||||
|
<h5 class="fw-bold h6 mb-3 text-start">Organizer</h5>
|
||||||
|
<img src="{{ event.creator.profile.get_avatar_url }}" class="rounded-circle mb-2" width="64" height="64">
|
||||||
|
<h6 class="fw-bold mb-1">{{ event.creator.get_full_name|default:event.creator.username }}</h6>
|
||||||
|
<p class="extra-small text-muted mb-3">{{ event.creator.profile.professional_headline }}</p>
|
||||||
|
<a href="{% url 'profile_detail' event.creator.username %}" class="btn btn-outline-dark btn-sm rounded-pill w-100 extra-small">View Profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attendees Widget -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold h6 mb-3">Attendees ({{ going_count }})</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for rsvp in rsvps %}
|
||||||
|
{% if rsvp.status == 'going' %}
|
||||||
|
<a href="{% url 'profile_detail' rsvp.user.username %}" title="{{ rsvp.user.username }}">
|
||||||
|
<img src="{{ rsvp.user.profile.get_avatar_url }}" class="rounded-circle" width="32" height="32" style="object-fit: cover;">
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<p class="extra-small text-muted mb-0">No one yet. Be the first!</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
126
core/templates/core/event_form.html
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h2 class="fw-bold mb-4">{{ title }}</h2>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Session Title*</label>
|
||||||
|
{{ form.title }}
|
||||||
|
{{ form.title.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Description*</label>
|
||||||
|
{{ form.description }}
|
||||||
|
{{ form.description.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date & Time -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Start Date & Time*</label>
|
||||||
|
{{ form.start_datetime }}
|
||||||
|
{{ form.start_datetime.errors }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">End Date & Time</label>
|
||||||
|
{{ form.end_datetime }}
|
||||||
|
{{ form.end_datetime.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Timezone</label>
|
||||||
|
{{ form.timezone }}
|
||||||
|
{{ form.timezone.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Location Name*</label>
|
||||||
|
{{ form.location_name }}
|
||||||
|
{{ form.location_name.errors }}
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Address</label>
|
||||||
|
{{ form.location_address }}
|
||||||
|
{{ form.location_address.errors }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">City</label>
|
||||||
|
{{ form.city }}
|
||||||
|
{{ form.city.errors }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">State</label>
|
||||||
|
{{ form.state }}
|
||||||
|
{{ form.state.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Online Info -->
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
{{ form.is_online }}
|
||||||
|
<label class="form-check-label small fw-bold" for="{{ form.is_online.id_for_label }}">This is an online session</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
...
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-5">
|
||||||
|
<a href="{% if event %}{% url 'event_detail' event.id %}{% else %}{% url 'events' %}{% endif %}" class="btn btn-link text-muted text-decoration-none">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary-cg rounded-pill px-5">Save Session</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Online Meeting URL</label>
|
||||||
|
{{ form.online_url }}
|
||||||
|
{{ form.online_url.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<div class="col-md-6 mt-4">
|
||||||
|
<label class="form-label small fw-bold">Visibility</label>
|
||||||
|
{{ form.visibility }}
|
||||||
|
{{ form.visibility.errors }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mt-4">
|
||||||
|
<label class="form-label small fw-bold">Capacity</label>
|
||||||
|
{{ form.capacity }}
|
||||||
|
{{ form.capacity.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<label class="form-label small fw-bold">Cover Image</label>
|
||||||
|
{{ form.cover_image }}
|
||||||
|
{{ form.cover_image.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<label class="form-label small fw-bold">Tags</label>
|
||||||
|
{{ form.tags }}
|
||||||
|
{{ form.tags.errors }}
|
||||||
|
<p class="extra-small text-muted mt-1">Hold Ctrl (or Cmd) to select multiple tags.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-5">
|
||||||
|
<a href="{% if event %}{% url 'event_detail' event.id %}{% else %}{% url 'events' %}{% endif %}" class="btn btn-link text-muted text-decoration-none">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary-cg rounded-pill px-5">Save Event</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
106
core/templates/core/events_list.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Sessions | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid dashboard-container px-lg-5">
|
||||||
|
<div class="row">
|
||||||
|
<!-- LEFT SIDEBAR -->
|
||||||
|
<div class="col-lg-2 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link"><i class="bi bi-compass"></i> Teammates</a>
|
||||||
|
<a href="{% url 'matches' %}" class="nav-link"><i class="bi bi-people"></i> My Teammates</a>
|
||||||
|
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||||
|
<a href="{% url 'events' %}" class="nav-link active"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||||
|
<a href="{% url 'groups:hub' %}" class="nav-link"><i class="bi bi-grid"></i> Squads</a>
|
||||||
|
<a href="{% url 'my_posts' %}" class="nav-link"><i class="bi bi-journal-text"></i> My Posts</a>
|
||||||
|
<a href="{% url 'my_profile' %}" class="nav-link"><i class="bi bi-person"></i> Profile</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN CONTENT -->
|
||||||
|
<div class="col-lg-10">
|
||||||
|
<div class="d-flex justify-content-between align-items-end mb-5 pt-2">
|
||||||
|
<div>
|
||||||
|
<h1 class="h2 mb-2">Sessions</h1>
|
||||||
|
<p class="text-muted mb-0">Coordinate and play with your teammates.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'event_create' %}" class="btn btn-primary-cg">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i> Create Session
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs & Search -->
|
||||||
|
<div class="card border-0 shadow-soft rounded-cg mb-5">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="row align-items-center g-4">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="d-flex gap-3 overflow-auto pb-1">
|
||||||
|
<a href="?tab=upcoming" class="btn btn-sm {% if tab == 'upcoming' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Upcoming</a>
|
||||||
|
<a href="?tab=calendar" class="btn btn-sm {% if tab == 'calendar' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Calendar</a>
|
||||||
|
<a href="?tab=attending" class="btn btn-sm {% if tab == 'attending' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Attending</a>
|
||||||
|
<a href="?tab=mine" class="btn btn-sm {% if tab == 'mine' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">My Sessions</a>
|
||||||
|
<a href="?tab=past" class="btn btn-sm {% if tab == 'past' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Past</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<form action="" method="GET" class="input-group">
|
||||||
|
<input type="hidden" name="tab" value="{{ tab }}">
|
||||||
|
<input type="text" name="q" class="form-control border-0 bg-secondary-section rounded-start-cg px-4" placeholder="Search sessions..." value="{{ query }}">
|
||||||
|
<button class="btn btn-outline-cg border-0 bg-secondary-section rounded-end-cg px-4" type="submit"><i class="bi bi-search text-muted"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Events Grid -->
|
||||||
|
<div class="row g-5">
|
||||||
|
{% if tab == 'calendar' %}
|
||||||
|
{% for date, day_events in calendar_events.items %}
|
||||||
|
<div class="col-12 mb-5">
|
||||||
|
<div class="d-flex align-items-center gap-4 mb-4">
|
||||||
|
<h4 class="mb-0 text-slate opacity-75" style="min-width: 140px;">{{ date|date:"D, M d" }}</h4>
|
||||||
|
<div class="flex-grow-1 border-top" style="border-color: var(--cg-divider) !important;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-5">
|
||||||
|
{% for event in day_events %}
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
{% include "core/includes/event_card.html" with event=event %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||||
|
<i class="bi bi-calendar-x display-1 text-muted opacity-25 mb-4 d-block"></i>
|
||||||
|
<h4 class="fw-bold">No sessions scheduled</h4>
|
||||||
|
<p class="text-muted">Stay tuned for upcoming squad sessions.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% for event in events %}
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
{% include "core/includes/event_card.html" with event=event %}
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||||
|
<i class="bi bi-calendar-x display-1 text-muted opacity-25 mb-4 d-block"></i>
|
||||||
|
<h4 class="fw-bold">No sessions found</h4>
|
||||||
|
<p class="text-muted">Try adjusting your filters or search query.</p>
|
||||||
|
<a href="{% url 'event_create' %}" class="btn btn-primary-cg mt-4">Create a session!</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
40
core/templates/core/inbox.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Messages | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h2 class="h3 mb-4">Conversations</h2>
|
||||||
|
|
||||||
|
<div class="list-group shadow-sm rounded-3 overflow-hidden">
|
||||||
|
{% for partner in partners %}
|
||||||
|
<a href="{% url 'chat_detail' partner.username %}" class="list-group-item list-group-item-action p-3 border-0 border-bottom">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img src="{{ partner.profile.avatar_url|default:'https://ui-avatars.com/api/?name='|add:partner.first_name }}" class="rounded-circle me-3" width="50" height="50" alt="{{ partner.username }}">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0 fw-bold">{{ partner.get_full_name|default:partner.username }}</h6>
|
||||||
|
<small class="text-muted">{{ partner.last_message.timestamp|date:"M d" }}</small>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted mb-0 small text-truncate" style="max-width: 300px;">
|
||||||
|
{% if partner.last_message.sender == request.user %}You: {% endif %}
|
||||||
|
{{ partner.last_message.body }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if not partner.last_message.is_read and partner.last_message.recipient == request.user %}
|
||||||
|
<span class="badge bg-primary rounded-pill">New</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<div class="list-group-item text-center py-5">
|
||||||
|
<p class="text-muted mb-0">No conversations yet. Connect with someone from the Discovery Hub!</p>
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-primary-cg btn-sm mt-3">Browse Profiles</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
40
core/templates/core/includes/event_card.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<div class="card border-0 shadow-soft rounded-cg h-100 overflow-hidden event-card">
|
||||||
|
{% if event.cover_image %}
|
||||||
|
<img src="{{ event.cover_image.url }}" class="card-img-top" style="height: 180px; object-fit: cover;">
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-secondary-section d-flex align-items-center justify-content-center" style="height: 180px;">
|
||||||
|
<i class="bi bi-calendar-event text-muted opacity-25" style="font-size: 3rem;"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="intent-badge border-0" style="background: var(--cg-sand-primary); font-size: 0.7rem;">
|
||||||
|
{{ event.get_visibility_display }}
|
||||||
|
</span>
|
||||||
|
<p class="small fw-bold text-slate mb-0 opacity-75">
|
||||||
|
{{ event.start_datetime|date:"M d" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold h6 mb-2">
|
||||||
|
<a href="{% url 'event_detail' event.id %}" class="text-slate text-decoration-none stretched-link">{{ event.title }}</a>
|
||||||
|
</h5>
|
||||||
|
<p class="text-muted small mb-4 line-clamp-2">{{ event.description }}</p>
|
||||||
|
|
||||||
|
<div class="mt-auto">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-4">
|
||||||
|
<i class="bi bi-geo-alt text-muted small"></i>
|
||||||
|
<span class="small text-muted text-truncate">{{ event.location_name }}{% if event.city %}, {{ event.city }}{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center pt-3 border-top">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img src="{{ event.creator.profile.get_avatar_url }}" class="rounded-circle me-2" width="28" height="28" style="object-fit: cover;">
|
||||||
|
<span class="small text-muted">By {{ event.creator.first_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<i class="bi bi-people me-1"></i> {{ event.rsvp_count }}{% if event.capacity %}/{{ event.capacity }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,145 +1,294 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load social_filters %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}Explore | Roster{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color-start: #6a11cb;
|
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
{% if user.is_authenticated %}
|
||||||
<div class="card">
|
<div class="container-fluid dashboard-container px-lg-5">
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<div class="row">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<!-- LEFT SIDEBAR (Sticky) -->
|
||||||
<span class="sr-only">Loading…</span>
|
<div class="col-lg-2 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}"><i class="bi bi-compass"></i> Explore</a>
|
||||||
|
<a href="{% url 'matches' %}" class="nav-link {% if request.resolver_match.url_name == 'matches' %}active{% endif %}"><i class="bi bi-shield-check"></i> My Roster</a>
|
||||||
|
<a href="{% url 'inbox' %}" class="nav-link {% if request.resolver_match.url_name == 'inbox' or request.resolver_match.url_name == 'chat_detail' %}active{% endif %}"><i class="bi bi-chat-left-text"></i> Messages</a>
|
||||||
|
<a href="{% url 'events' %}" class="nav-link {% if request.resolver_match.url_name == 'events' %}active{% endif %}"><i class="bi bi-lightning-charge"></i> Sessions</a>
|
||||||
|
<a href="{% url 'groups:hub' %}" class="nav-link {% if 'groups' in request.path %}active{% endif %}"><i class="bi bi-grid-1x2"></i> Squads</a>
|
||||||
|
<a href="{% url 'my_posts' %}" class="nav-link {% if request.resolver_match.url_name == 'my_posts' %}active{% endif %}"><i class="bi bi-journal-text"></i> My Posts</a>
|
||||||
|
<a href="{% url 'my_profile' %}" class="nav-link {% if request.resolver_match.url_name == 'my_profile' or request.resolver_match.url_name == 'profile_detail' %}active{% endif %}"><i class="bi bi-person-badge"></i> Profile</a>
|
||||||
|
<a href="{% url 'settings' %}" class="nav-link {% if request.resolver_match.url_name == 'settings' %}active{% endif %}"><i class="bi bi-gear"></i> Settings</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CENTER COLUMN -->
|
||||||
|
<div class="col-lg-7 col-md-12">
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<div class="dashboard-section pt-2">
|
||||||
|
<h1 class="welcome-header mb-1">Welcome back, {{ user.first_name|default:user.username }}</h1>
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="text-muted small"><i class="bi bi-geo-alt me-1"></i> {{ user.profile.location_city|default:"Location not set" }}</span>
|
||||||
|
{% if user.profile.primary_game %}
|
||||||
|
<span class="intent-badge">{{ user.profile.primary_game.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Composer -->
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="card p-4">
|
||||||
|
<form action="{% url 'create_post' %}" method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex gap-4 mb-4">
|
||||||
|
<img src="{{ user.profile.get_avatar_url }}" class="rounded-circle" width="48" height="48" style="object-fit: cover; opacity: 0.9; border: 2px solid var(--r-cyan);">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<label class="small fw-bold text-muted mb-2">Share your latest beauty breakthrough or find your ritual squad</label>
|
||||||
|
<textarea name="content" class="form-control border-0 bg-transparent" placeholder="What's your ritual today? Sharing a discovery or looking for advice..." rows="2" style="resize: none;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center pt-2">
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<select name="post_type" class="form-select form-select-sm border-0 px-3" style="width: auto; font-size: 0.8rem;">
|
||||||
|
{% for code, label in post_types %}
|
||||||
|
<option value="{{ code }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="upload-btn-wrapper">
|
||||||
|
<button type="button" class="btn btn-outline-cg border-0 btn-sm px-3">
|
||||||
|
<i class="bi bi-image text-muted"></i>
|
||||||
|
</button>
|
||||||
|
<input type="file" name="image" accept="image/*" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary-cg btn-sm px-4">Post</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggested Roster -->
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="section-header mb-0">Suggested Roster</h2>
|
||||||
|
<a href="{% url 'home' %}" class="small text-decoration-none fw-bold text-cyan">View All</a>
|
||||||
|
</div>
|
||||||
|
<div class="horizontal-scroll">
|
||||||
|
{% for profile in suggested_members %}
|
||||||
|
<div class="suggestion-card text-center">
|
||||||
|
<a href="{% url 'profile_detail' profile.user.username %}" class="text-decoration-none">
|
||||||
|
<img src="{{ profile.get_avatar_url }}" class="rounded-circle mb-3" width="56" height="56" style="object-fit: cover; border: 2px solid var(--r-cyan);">
|
||||||
|
<h6 class="mb-1 small fw-bold text-truncate text-white">{{ profile.user.first_name }} {{ profile.user.last_name|slice:":1" }}.</h6>
|
||||||
|
<p class="small text-muted mb-3 text-truncate">{{ profile.primary_game.name|default:"Beauty Enthusiast" }}</p>
|
||||||
|
</a>
|
||||||
|
<div class="d-grid">
|
||||||
|
{% if profile.user.id in following_ids %}
|
||||||
|
<a href="{% url 'toggle_follow' profile.user.username %}" class="btn btn-outline-cg btn-sm py-1">Following</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'toggle_follow' profile.user.username %}" class="btn btn-primary-cg btn-sm py-1">Follow</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Activity Feed -->
|
||||||
|
<div class="feed dashboard-section">
|
||||||
|
<h2 class="section-header mb-4">Activity Feed</h2>
|
||||||
|
{% for post in posts %}
|
||||||
|
<div class="card mb-5">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img src="{{ post.author.profile.get_avatar_url }}" class="rounded-circle me-3" width="44" height="44" style="object-fit: cover;">
|
||||||
|
<div>
|
||||||
|
<h4 class="h6 mb-0 fw-bold">{{ post.author.get_full_name|default:post.author.username }}</h4>
|
||||||
|
<p class="text-muted extra-small mb-0">{{ post.timestamp|timesince }} ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="post-badge">{{ post.get_post_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4">{{ post.content }}</p>
|
||||||
|
{% if post.image %}
|
||||||
|
<img src="{{ post.image.url }}" class="img-fluid rounded-cg mb-4 w-100" style="max-height: 400px; object-fit: cover;">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Post Actions -->
|
||||||
|
<div class="d-flex gap-4 border-top pt-4">
|
||||||
|
<a href="{% url 'toggle_reaction' post.id %}?type=heart" class="text-decoration-none {% if post|reacted_by:user %}text-danger{% else %}text-muted{% endif %} small fw-bold">
|
||||||
|
<i class="bi bi-heart{% if post|reacted_by:user %}-fill{% endif %} me-2"></i> {{ post.reactions.count }}
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-link p-0 text-muted text-decoration-none small fw-bold" type="button" data-bs-toggle="collapse" data-bs-target="#comments-{{ post.id }}">
|
||||||
|
<i class="bi bi-chat me-2"></i> {{ post.comments.count }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments Section -->
|
||||||
|
<div class="collapse mt-4" id="comments-{{ post.id }}">
|
||||||
|
<div class="bg-secondary-section p-4 rounded-cg">
|
||||||
|
{% for comment in post.comments.all %}
|
||||||
|
<div class="d-flex gap-3 mb-3">
|
||||||
|
<img src="{{ comment.author.profile.get_avatar_url }}" class="rounded-circle" width="32" height="32" style="object-fit: cover;">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="bg-white p-3 rounded-cg shadow-soft">
|
||||||
|
<p class="small fw-bold mb-1">{{ comment.author.username }}</p>
|
||||||
|
<p class="small mb-0 text-muted">{{ comment.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<form action="{% url 'add_comment' post.id %}" method="POST" class="mt-3">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="content" class="form-control border-0 rounded-start-cg px-4 bg-white" placeholder="Write a comment...">
|
||||||
|
<button class="btn btn-primary-cg rounded-end-cg px-4" type="submit">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||||
|
<p class="text-muted">No posts yet. Start the conversation!</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT SIDEBAR (Sticky) -->
|
||||||
|
<div class="col-lg-3 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar pt-2">
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="stat-widget">
|
||||||
|
<h2 class="section-header mb-4" style="font-size: 1.1rem;">Your Momentum</h2>
|
||||||
|
<div class="stat-item border-0 mb-2 d-flex justify-content-between">
|
||||||
|
<span class="small text-muted">Unread Messages</span>
|
||||||
|
<span class="badge bg-light text-dark rounded-pill small">{{ stats.unread_messages }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item border-0 mb-2 d-flex justify-content-between">
|
||||||
|
<span class="small text-muted">Accountability Streak</span>
|
||||||
|
<span class="fw-bold text-sage small">{{ stats.streak }} 🔥</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item border-0 mb-3 d-flex justify-content-between">
|
||||||
|
<span class="small text-muted">Upcoming Events</span>
|
||||||
|
<span class="fw-bold small">{{ stats.upcoming_events_count }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 pt-3 border-top">
|
||||||
|
<div class="d-flex justify-content-between small mb-2">
|
||||||
|
<span class="text-muted">Profile Completion</span>
|
||||||
|
<span class="fw-bold">{{ stats.completion_percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 4px; background-color: rgba(0,0,0,0.05);">
|
||||||
|
<div class="progress-bar" style="width: {{ stats.completion_percentage }}%; background-color: var(--cg-slate);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="stat-widget">
|
||||||
|
<h2 class="section-header mb-4" style="font-size: 1.1rem;">Quick Actions</h2>
|
||||||
|
<div class="d-grid gap-3">
|
||||||
|
<a href="{% url 'event_create' %}" class="btn btn-outline-cg btn-sm text-start py-2"><i class="bi bi-calendar-plus me-3"></i> Create Session</a>
|
||||||
|
<a href="{% url 'matches' %}" class="btn btn-outline-cg btn-sm text-start py-2"><i class="bi bi-person-plus me-3"></i> Find Teammate</a>
|
||||||
|
<a href="{% url 'groups:create' %}" class="btn btn-outline-cg btn-sm text-start py-2"><i class="bi bi-plus-circle me-3"></i> Start Squad</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggested Events -->
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="stat-widget">
|
||||||
|
<h2 class="section-header mb-3 border-bottom pb-2" style="font-size: 1.1rem;">Upcoming Sessions</h2>
|
||||||
|
{% for event in suggested_events %}
|
||||||
|
<div class="mb-3 pb-2 border-bottom">
|
||||||
|
<p class="small fw-bold mb-0 text-truncate">{{ event.title }}</p>
|
||||||
|
<p class="extra-small text-muted mb-0">{{ event.start_datetime|date:"M d, P" }}</p>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="extra-small text-muted text-center py-2">No upcoming sessions found.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
</div>
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
|
||||||
<p class="runtime">
|
{% else %}
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
<!-- Anonymous Landing Page -->
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
<section class="hero-section py-5">
|
||||||
</p>
|
<div class="container py-5">
|
||||||
</div>
|
<div class="row justify-content-center">
|
||||||
</main>
|
<div class="col-lg-10 text-center">
|
||||||
<footer>
|
<h1 class="hero-title">Build your ultimate Roster.</h1>
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
<p class="lead text-muted mb-5 px-lg-5" style="font-size: 1.5rem;">Connect with the best in personalized beauty. Find your ritual, share your journey, and build your squad.</p>
|
||||||
</footer>
|
<div class="d-flex justify-content-center gap-4">
|
||||||
{% endblock %}
|
<a href="{% url 'signup' %}" class="btn btn-neon">Join Roster</a>
|
||||||
|
<a href="{% url 'about' %}" class="btn btn-outline-cg">Learn More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-end mb-5 flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-metallic h1 mb-2">Discover Experts & Enthusiasts</h2>
|
||||||
|
<p class="text-muted mb-0">Personalities aligned with your beauty goals and interests.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex overflow-auto pb-2 gap-3">
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-sm {% if not current_intent %}btn-neon{% else %}btn-outline-cg border-0{% endif %} rounded-pill px-4">All Profiles</a>
|
||||||
|
{% for intent in intents %}
|
||||||
|
<a href="?intent={{ intent.name }}" class="btn btn-sm {% if current_intent == intent.name %}btn-neon{% else %}btn-outline-cg border-0{% endif %} rounded-pill px-4">{{ intent.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-5">
|
||||||
|
{% for profile in profiles %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card p-5 h-100 d-flex flex-column border-0">
|
||||||
|
<div class="d-flex align-items-start mb-4">
|
||||||
|
<img src="{{ profile.get_avatar_url }}" alt="{{ profile.user.get_full_name }}" class="profile-avatar me-4" style="width: 64px; height: 64px; border: 2px solid var(--r-cyan);">
|
||||||
|
<div class="pt-1">
|
||||||
|
{% if profile.primary_game %}
|
||||||
|
<span class="intent-badge mb-2 d-inline-block">{{ profile.primary_game.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<h3 class="h5 mb-1 fw-bold text-white">{{ profile.user.first_name }} {{ profile.user.last_name|slice:":1" }}.</h3>
|
||||||
|
<p class="text-muted small mb-0"><i class="bi bi-geo-alt me-2"></i> {{ profile.location_city }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="fw-bold text-cyan mb-3">{{ profile.platform|upper|default:"BEAUTY" }} • {{ profile.rank|default:"Enthusiast" }}</p>
|
||||||
|
<p class="text-muted small mb-4 line-clamp-3">{{ profile.bio }}</p>
|
||||||
|
|
||||||
|
<div class="mb-5 d-flex flex-wrap gap-2 mt-auto">
|
||||||
|
{% for intent in profile.intents.all %}
|
||||||
|
<span class="intent-badge">{{ intent.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid mt-2">
|
||||||
|
<a href="{% url 'signup' %}" class="btn btn-outline-cg">View Full Roster</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<p class="text-muted">No profiles found yet.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
134
core/templates/core/matches.html
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load social_filters %}
|
||||||
|
|
||||||
|
{% block title %}Teammates | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid dashboard-container px-lg-5">
|
||||||
|
<div class="row">
|
||||||
|
<!-- LEFT SIDEBAR -->
|
||||||
|
<div class="col-lg-2 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}"><i class="bi bi-compass"></i> Teammates</a>
|
||||||
|
<a href="{% url 'matches' %}" class="nav-link {% if 'matches' in request.path %}active{% endif %}"><i class="bi bi-people"></i> My Teammates</a>
|
||||||
|
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||||
|
<a href="{% url 'events' %}" class="nav-link"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||||
|
<a href="{% url 'groups:hub' %}" class="nav-link"><i class="bi bi-grid"></i> Squads</a>
|
||||||
|
<a href="{% url 'my_posts' %}" class="nav-link"><i class="bi bi-journal-text"></i> My Posts</a>
|
||||||
|
<a href="{% url 'my_profile' %}" class="nav-link"><i class="bi bi-person"></i> Profile</a>
|
||||||
|
<a href="{% url 'settings' %}" class="nav-link"><i class="bi bi-gear"></i> Settings</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CENTER COLUMN -->
|
||||||
|
<div class="col-lg-10 col-md-12">
|
||||||
|
<div class="mb-5 pt-2">
|
||||||
|
<h1 class="h2 mb-2">My Teammates</h1>
|
||||||
|
<p class="text-muted mb-0">Manage your gaming teammates and squads.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation & Filters -->
|
||||||
|
<div class="card border-0 shadow-soft rounded-cg mb-5">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-4">
|
||||||
|
<div class="d-flex overflow-auto pb-1 gap-3">
|
||||||
|
<a href="{% url 'match_mutual' %}" class="btn btn-sm {% if current_tab == 'mutual' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Mutual</a>
|
||||||
|
<a href="{% url 'match_requests' %}" class="btn btn-sm {% if current_tab == 'requests' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">
|
||||||
|
Requests
|
||||||
|
{% if stats.pending_connections > 0 %}
|
||||||
|
<span class="badge bg-white text-slate rounded-pill ms-2" style="font-size: 0.7rem;">{{ stats.pending_connections }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'matches' %}?tab=sent" class="btn btn-sm {% if current_tab == 'sent' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Sent</a>
|
||||||
|
<a href="{% url 'match_liked' %}" class="btn btn-sm {% if current_tab == 'liked' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Liked</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search & Filter -->
|
||||||
|
<form class="d-flex gap-3 flex-grow-1 flex-lg-grow-0" method="GET" style="min-width: 300px;">
|
||||||
|
<input type="hidden" name="tab" value="{{ current_tab }}">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control border-0 bg-secondary-section rounded-start-cg px-4" placeholder="Search..." value="{{ search_query }}">
|
||||||
|
<button class="btn btn-outline-cg border-0 bg-secondary-section rounded-end-cg px-3" type="submit"><i class="bi bi-search text-muted"></i></button>
|
||||||
|
</div>
|
||||||
|
<select name="intent" class="form-select border-0 bg-secondary-section rounded-cg px-4" style="width: auto;" onchange="this.form.submit()">
|
||||||
|
<option value="">Intents</option>
|
||||||
|
{% for intent in intents %}
|
||||||
|
<option value="{{ intent.name }}" {% if current_intent == intent.name %}selected{% endif %}>{{ intent.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Match Grid -->
|
||||||
|
<div class="row g-5">
|
||||||
|
{% for match_user in matches_list %}
|
||||||
|
<div class="col-md-6 col-lg-4 col-xl-3">
|
||||||
|
<div class="profile-card p-5 border-0 shadow-soft d-flex flex-column h-100">
|
||||||
|
<div class="d-flex align-items-start mb-4">
|
||||||
|
<img src="{{ match_user.profile.get_avatar_url }}" alt="{{ match_user.get_full_name }}" class="rounded-circle me-3" style="width: 56px; height: 56px; object-fit: cover;">
|
||||||
|
<div class="pt-1 overflow-hidden">
|
||||||
|
{% if match_user.profile.primary_game %}
|
||||||
|
<span class="intent-badge mb-1 d-inline-block" style="font-size: 0.65rem;">{{ match_user.profile.primary_game.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<h3 class="h6 mb-0 fw-bold text-truncate">{{ match_user.get_full_name|default:match_user.username }}</h3>
|
||||||
|
<p class="text-muted small mb-0 text-truncate"><i class="bi bi-geo-alt me-1"></i> {{ match_user.profile.location_city|default:"No Location" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="fw-bold small text-slate mb-2 text-truncate">{{ match_user.profile.platform|upper|default:"GAMER" }} • {{ match_user.profile.rank|default:"Casual" }}</p>
|
||||||
|
<p class="text-muted small mb-4 line-clamp-3" style="font-size: 0.8rem;">{{ match_user.profile.bio|default:"No bio yet." }}</p>
|
||||||
|
|
||||||
|
<div class="mb-5 d-flex flex-wrap gap-2 mt-auto">
|
||||||
|
{% for intent in match_user.profile.intents.all %}
|
||||||
|
<span class="intent-badge" style="font-size: 0.65rem; padding: 2px 10px;">{{ intent.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-3">
|
||||||
|
{% if current_tab == 'mutual' %}
|
||||||
|
<a href="{% url 'chat_detail' match_user.username %}" class="btn btn-primary-cg btn-sm">Message</a>
|
||||||
|
<div class="dropdown d-grid">
|
||||||
|
<button class="btn btn-outline-cg btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">Manage</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow-soft border-0 rounded-cg p-2">
|
||||||
|
<li><a class="dropdown-item small rounded-cg py-2" href="{% url 'profile_detail' match_user.username %}">View Profile</a></li>
|
||||||
|
<li><hr class="dropdown-divider opacity-50"></li>
|
||||||
|
<li><a class="dropdown-item text-danger small rounded-cg py-2" href="{% url 'block_user' match_user.username %}" onclick="return confirm('Block this user?')">Block & Report</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% elif current_tab == 'requests' %}
|
||||||
|
{% with request_id=request_map|get_item:match_user.id %}
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<a href="{% url 'handle_match_request' request_id %}?action=accept" class="btn btn-primary-cg btn-sm flex-grow-1">Accept</a>
|
||||||
|
<a href="{% url 'handle_match_request' request_id %}?action=decline" class="btn btn-outline-cg btn-sm flex-grow-1">Decline</a>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% elif current_tab == 'sent' %}
|
||||||
|
<a href="{% url 'cancel_match_request' match_user.username %}" class="btn btn-outline-cg btn-sm border-0 bg-secondary-section">Cancel Request</a>
|
||||||
|
{% elif current_tab == 'liked' %}
|
||||||
|
<a href="{% url 'send_match_request' match_user.username %}" class="btn btn-primary-cg btn-sm">Send Request</a>
|
||||||
|
<a href="{% url 'toggle_like' match_user.username %}" class="btn btn-outline-cg btn-sm">Unlike</a>
|
||||||
|
{% elif current_tab == 'blocked' %}
|
||||||
|
<a href="#" class="btn btn-outline-cg btn-sm disabled opacity-50">Blocked</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12 text-center py-5 bg-white rounded-cg shadow-soft">
|
||||||
|
<i class="bi bi-people text-muted opacity-25 display-1 mb-4 d-block"></i>
|
||||||
|
<h4 class="fw-bold">No {{ current_tab }} found.</h4>
|
||||||
|
<p class="text-muted">Try adjusting your filters or search query.</p>
|
||||||
|
{% if current_tab == 'mutual' %}
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-primary-cg mt-4">Find Teammates</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
54
core/templates/core/onboarding.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Player Setup - Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center text-center mb-5">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h1 class="brand-font display-5 mb-3">Player Setup</h1>
|
||||||
|
<p class="text-muted">Set up your profile to find your perfect squad.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
|
<div class="card border-0 shadow-soft p-4 rounded-cg">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">Gamer Tag</label>
|
||||||
|
<input type="text" name="gamer_tag" class="form-control" placeholder="e.g. MasterGamer2026" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">Primary Game</label>
|
||||||
|
<select name="primary_game" class="form-select" required>
|
||||||
|
<option value="">Select a game</option>
|
||||||
|
{% for game in games %}
|
||||||
|
<option value="{{ game.id }}">{{ game.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">Platform</label>
|
||||||
|
<select name="platform" class="form-select" required>
|
||||||
|
{% for code, name in platforms %}
|
||||||
|
<option value="{{ code }}">{{ name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">About You</label>
|
||||||
|
<textarea name="bio" class="form-control" rows="4" placeholder="Tell us about your playstyle, availability, and vibe..." required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary-cg w-100 py-2">Start Matching</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
19
core/templates/core/placeholder.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }} | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 text-center">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-5">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="bi bi-rocket-takeoff text-primary-cg" style="font-size: 4rem;"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="fw-bold mb-3">{{ title }}</h1>
|
||||||
|
<p class="lead text-muted mb-4">We're currently building the {{ title }} feature to help you connect even better with your community. Stay tuned!</p>
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-primary-cg btn-lg rounded-pill px-5">Back to Discover</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
162
core/templates/core/profile_detail.html
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load social_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<!-- Profile Header Card -->
|
||||||
|
<div class="card border-0 shadow-soft rounded-cg overflow-hidden mb-5">
|
||||||
|
<div class="bg-secondary-section py-5 text-center position-relative" style="height: 160px;">
|
||||||
|
<div class="position-absolute top-100 start-50 translate-middle">
|
||||||
|
<img src="{{ target_user.profile.get_avatar_url }}"
|
||||||
|
class="rounded-circle border border-4 border-white shadow-soft"
|
||||||
|
style="width: 160px; height: 160px; object-fit: cover;"
|
||||||
|
alt="{{ target_user.username }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pt-5 mt-5 text-center px-lg-5">
|
||||||
|
<h1 class="mb-1">{{ target_user.first_name }} {{ target_user.last_name }}</h1>
|
||||||
|
<p class="text-muted mb-4 small">@{{ target_user.username }}</p>
|
||||||
|
|
||||||
|
{% if target_user.profile.professional_headline %}
|
||||||
|
<h5 class="text-slate mb-4 opacity-75">{{ target_user.profile.professional_headline }}</h5>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center gap-3 mb-5">
|
||||||
|
<span class="intent-badge bg-secondary-section border-0 px-3">
|
||||||
|
<i class="bi bi-geo-alt me-2"></i> {{ target_user.profile.location_city|default:"Location not set" }}
|
||||||
|
</span>
|
||||||
|
{% if target_user.profile.primary_game %}
|
||||||
|
<span class="intent-badge px-3">
|
||||||
|
<i class="bi bi-controller me-2"></i> {{ target_user.profile.primary_game.name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if target_user.profile.platform %}
|
||||||
|
<span class="intent-badge px-3">
|
||||||
|
<i class="bi bi-display me-2"></i> {{ target_user.profile.get_platform_display }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-md-5 mb-5 text-slate opacity-75 lead" style="font-size: 1.1rem; line-height: 1.8;">
|
||||||
|
{{ target_user.profile.bio|linebreaks }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if request.user == target_user %}
|
||||||
|
<div class="d-flex justify-content-center gap-3 mb-4">
|
||||||
|
<a href="{% url 'edit_profile' %}" class="btn btn-primary-cg px-5">
|
||||||
|
<i class="bi bi-pencil me-2"></i>Edit Profile
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'settings' %}" class="btn btn-outline-cg px-5">
|
||||||
|
<i class="bi bi-gear me-2"></i>Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="d-flex justify-content-center gap-3 mb-4">
|
||||||
|
{% if is_following %}
|
||||||
|
<a href="{% url 'toggle_follow' target_user.username %}" class="btn btn-outline-cg px-5">
|
||||||
|
<i class="bi bi-person-check-fill me-2"></i>Following
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'toggle_follow' target_user.username %}" class="btn btn-primary-cg px-5">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>Follow
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'chat_detail' target_user.username %}" class="btn btn-outline-cg px-5">
|
||||||
|
<i class="bi bi-chat-dots me-2"></i>Message
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer bg-white border-top-0 py-5 px-lg-5 border-top">
|
||||||
|
<div class="row g-4 text-center">
|
||||||
|
<div class="col-md-4 border-end border-light">
|
||||||
|
<h6 class="text-muted text-uppercase extra-small fw-bold mb-3 ls-1">Looking For</h6>
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||||||
|
{% for intent in target_user.profile.intents.all %}
|
||||||
|
<span class="intent-badge border-0" style="background: var(--cg-sand-primary);">
|
||||||
|
{{ intent.name }}
|
||||||
|
</span>
|
||||||
|
{% empty %}
|
||||||
|
<small class="text-muted small">No intents shared</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 border-end border-light">
|
||||||
|
<h6 class="text-muted text-uppercase extra-small fw-bold mb-3 ls-1">Values</h6>
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||||||
|
{% for tag in target_user.profile.value_tags.all %}
|
||||||
|
<span class="intent-badge border-0" style="background: var(--cg-sand-secondary);">
|
||||||
|
#{{ tag.name }}
|
||||||
|
</span>
|
||||||
|
{% empty %}
|
||||||
|
<small class="text-muted small">No values shared</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h6 class="text-muted text-uppercase extra-small fw-bold mb-3 ls-1">Gamer Stats</h6>
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||||||
|
{% if target_user.profile.rank %}
|
||||||
|
<span class="intent-badge border-0" style="background: var(--cg-sand-primary);">
|
||||||
|
Rank: {{ target_user.profile.rank }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if target_user.profile.preferred_role %}
|
||||||
|
<span class="intent-badge border-0" style="background: var(--cg-sand-secondary);">
|
||||||
|
Role: {{ target_user.profile.preferred_role }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="intent-badge border-0 text-sage" style="background: var(--cg-sand-primary);">
|
||||||
|
{{ target_user.profile.accountability_streak }} 🔥 Streak
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Activity Feed -->
|
||||||
|
<div class="feed-section">
|
||||||
|
<h3 class="mb-4">Recent Activity</h3>
|
||||||
|
{% for post in target_user.posts.all %}
|
||||||
|
<div class="card border-0 shadow-soft mb-5 rounded-cg">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img src="{{ post.author.profile.get_avatar_url }}" class="rounded-circle me-3" width="44" height="44" style="object-fit: cover;">
|
||||||
|
<div>
|
||||||
|
<h4 class="h6 mb-0 fw-bold">{{ post.author.get_full_name|default:post.author.username }}</h4>
|
||||||
|
<p class="text-muted extra-small mb-0">{{ post.timestamp|timesince }} ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="post-badge">{{ post.get_post_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4">{{ post.content }}</p>
|
||||||
|
{% if post.image %}
|
||||||
|
<img src="{{ post.image.url }}" class="img-fluid rounded-cg mb-4 w-100" style="max-height: 400px; object-fit: cover;">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex gap-4 border-top pt-4">
|
||||||
|
<a href="{% url 'toggle_reaction' post.id %}?type=heart" class="text-decoration-none {% if post|reacted_by:user %}text-danger{% else %}text-muted{% endif %} small fw-bold">
|
||||||
|
<i class="bi bi-heart{% if post|reacted_by:user %}-fill{% endif %} me-2"></i> {{ post.reactions.count }}
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-link p-0 text-muted text-decoration-none small fw-bold" type="button" data-bs-toggle="collapse" data-bs-target="#comments-{{ post.id }}">
|
||||||
|
<i class="bi bi-chat me-2"></i> {{ post.comments.count }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||||
|
<p class="text-muted mb-0">No posts yet.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
52
core/templates/core/settings.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Settings - Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-4 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm p-4">
|
||||||
|
<h3 class="brand-font h4 mb-4">Account Settings</h3>
|
||||||
|
<nav class="nav flex-column">
|
||||||
|
<a class="nav-link px-0 text-slate fw-bold" href="#">Preferences</a>
|
||||||
|
<a class="nav-link px-0 text-muted" href="#">Privacy & Visibility</a>
|
||||||
|
<a class="nav-link px-0 text-muted" href="#">Notifications</a>
|
||||||
|
<hr>
|
||||||
|
<form action="{% url 'logout' %}" method="post" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="nav-link px-0 text-danger border-0 bg-transparent">Log Out</button>
|
||||||
|
</form>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm p-4">
|
||||||
|
<h4 class="brand-font mb-4">Preferences</h4>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label d-block fw-bold">Two-Factor Authentication</label>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="two_factor" id="twoFactorSwitch" {% if profile.two_factor_enabled %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="twoFactorSwitch">Enable 2FA for extra security</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label d-block fw-bold">Email Verification</label>
|
||||||
|
{% if profile.is_email_verified %}
|
||||||
|
<span class="badge bg-success">Verified</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning text-dark">Pending</span>
|
||||||
|
<p class="small text-muted mt-2">Verification is simulated for MVP.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary-cg px-4">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
30
core/templates/registration/login.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Log In - Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card border-0 shadow-sm p-4">
|
||||||
|
<h2 class="brand-font text-center mb-4">Welcome back</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary-cg w-100">Log In</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p class="small text-muted">Don't have an account? <a href="{% url 'signup' %}">Get Started</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
56
core/templates/registration/signup.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Player Setup - Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card border-0 shadow-sm p-4">
|
||||||
|
<h2 class="brand-font text-center mb-4">Player Setup</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="text-danger small">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary-cg w-100">Sign Up</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p class="small text-muted">Already have an account? <a href="{% url 'login' %}">Log in</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #212529;
|
||||||
|
background-color: #fff;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
0
core/templatetags/__init__.py
Normal file
BIN
core/templatetags/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/templatetags/__pycache__/social_filters.cpython-311.pyc
Normal file
13
core/templatetags/social_filters.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter(name='reacted_by')
|
||||||
|
def reacted_by(post, user):
|
||||||
|
if user.is_authenticated:
|
||||||
|
return post.reactions.filter(user=user).exists()
|
||||||
|
return False
|
||||||
|
|
||||||
|
@register.filter(name='get_item')
|
||||||
|
def get_item(dictionary, key):
|
||||||
|
return dictionary.get(key)
|
||||||
56
core/urls.py
@ -1,7 +1,57 @@
|
|||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
from .views import home
|
from .views import (
|
||||||
|
home, about, signup, onboarding, settings_view,
|
||||||
|
get_started, inbox, chat_detail,
|
||||||
|
profile_view, profile_detail, edit_profile,
|
||||||
|
create_post, delete_post, add_comment, toggle_reaction, hide_post, toggle_follow,
|
||||||
|
matches, events, groups, my_posts,
|
||||||
|
send_match_request, handle_match_request, cancel_match_request, block_user, toggle_like,
|
||||||
|
event_detail, event_create, event_edit, event_delete, event_rsvp
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("about/", about, name="about"),
|
||||||
|
path("signup/", signup, name="signup"),
|
||||||
|
path("onboarding/", onboarding, name="onboarding"),
|
||||||
|
path("settings/", settings_view, name="settings"),
|
||||||
|
path("get-started/", get_started, name="get_started"),
|
||||||
|
path("messages/", inbox, name="inbox"),
|
||||||
|
path("messages/<str:username>/", chat_detail, name="chat_detail"),
|
||||||
|
path("profile/", profile_view, name="my_profile"),
|
||||||
|
path("profile/edit/", edit_profile, name="edit_profile"),
|
||||||
|
path("profile/<str:username>/", profile_detail, name="profile_detail"),
|
||||||
|
path("profile/<str:username>/follow/", toggle_follow, name="toggle_follow"),
|
||||||
|
|
||||||
|
# Matches
|
||||||
|
path("matches/", matches, name="matches"),
|
||||||
|
path("matches/requests/", matches, {'tab': 'requests'}, name="match_requests"),
|
||||||
|
path("matches/mutual/", matches, {'tab': 'mutual'}, name="match_mutual"),
|
||||||
|
path("matches/liked/", matches, {'tab': 'liked'}, name="match_liked"),
|
||||||
|
path("matches/blocked/", matches, {'tab': 'blocked'}, name="match_blocked"),
|
||||||
|
|
||||||
|
path("matches/request/<str:username>/", send_match_request, name="send_match_request"),
|
||||||
|
path("matches/handle/<int:request_id>/", handle_match_request, name="handle_match_request"),
|
||||||
|
path("matches/cancel/<str:username>/", cancel_match_request, name="cancel_match_request"),
|
||||||
|
path("matches/block/<str:username>/", block_user, name="block_user"),
|
||||||
|
path("matches/like/<str:username>/", toggle_like, name="toggle_like"),
|
||||||
|
path("events/", events, name="events"),
|
||||||
|
path("events/create/", event_create, name="event_create"),
|
||||||
|
path("events/<int:event_id>/", event_detail, name="event_detail"),
|
||||||
|
path("events/<int:event_id>/edit/", event_edit, name="event_edit"),
|
||||||
|
path("events/<int:event_id>/delete/", event_delete, name="event_delete"),
|
||||||
|
path("events/<int:event_id>/rsvp/", event_rsvp, name="event_rsvp"),
|
||||||
|
path("groups/", groups, name="groups"),
|
||||||
|
path("my-posts/", my_posts, name="my_posts"),
|
||||||
|
|
||||||
|
# Social URLs
|
||||||
|
path("post/create/", create_post, name="create_post"),
|
||||||
|
path("post/<int:post_id>/delete/", delete_post, name="delete_post"),
|
||||||
|
path("post/<int:post_id>/comment/", add_comment, name="add_comment"),
|
||||||
|
path("post/<int:post_id>/react/", toggle_reaction, name="toggle_reaction"),
|
||||||
|
path("post/<int:post_id>/hide/", hide_post, name="hide_post"),
|
||||||
|
|
||||||
|
# Auth URLs
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
587
core/views.py
@ -1,25 +1,580 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
from django import get_version as django_version
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.shortcuts import render
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from .models import (
|
||||||
|
Profile, Intent, ValueTag, Message, Post, Comment, Reaction,
|
||||||
|
HiddenPost, Follow, ConnectionRequest, Like, Match, Thread, Block,
|
||||||
|
Event, RSVP, EventTag, EventInvite
|
||||||
|
)
|
||||||
|
from .forms import EventForm
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
def get_dashboard_context(request):
|
||||||
|
"""Helper to get consistent context for dashboard-like views."""
|
||||||
|
stats = {}
|
||||||
|
suggested_members = []
|
||||||
|
suggested_events = []
|
||||||
|
following_ids = []
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
# Quick Stats
|
||||||
|
stats = {
|
||||||
|
'unread_messages': Message.objects.filter(recipient=request.user, is_read=False).count(),
|
||||||
|
'pending_connections': ConnectionRequest.objects.filter(to_user=request.user, status='pending').count(),
|
||||||
|
'upcoming_events_count': request.user.rsvps.filter(status='going', event__start_datetime__gt=timezone.now()).count(),
|
||||||
|
'completion_percentage': request.user.profile.profile_completion_percentage,
|
||||||
|
'streak': request.user.profile.accountability_streak,
|
||||||
|
'followers_count': request.user.profile.followers_count,
|
||||||
|
'following_count': request.user.profile.following_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
following_ids = list(Follow.objects.filter(follower=request.user).values_list('followed_id', flat=True))
|
||||||
|
|
||||||
|
# Suggestions
|
||||||
|
user_intents = request.user.profile.intents.all()
|
||||||
|
user_values = request.user.profile.value_tags.all()
|
||||||
|
suggested_members = Profile.objects.filter(
|
||||||
|
Q(intents__in=user_intents) | Q(value_tags__in=user_values)
|
||||||
|
).exclude(user=request.user).distinct()[:10]
|
||||||
|
|
||||||
|
# Suggested Events
|
||||||
|
suggested_events = Event.objects.filter(start_datetime__gt=timezone.now()).order_by('start_datetime')[:5]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"stats": stats,
|
||||||
|
"suggested_members": suggested_members,
|
||||||
|
"suggested_events": suggested_events,
|
||||||
|
"following_ids": following_ids,
|
||||||
|
"post_types": Post.POST_TYPE_CHOICES,
|
||||||
|
"current_time": timezone.now(),
|
||||||
|
"project_name": "Roster",
|
||||||
|
}
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
"""Render the landing screen or member dashboard."""
|
||||||
host_name = request.get_host().lower()
|
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
# Simple logic to seed data for the first run
|
||||||
now = timezone.now()
|
if not Intent.objects.exists():
|
||||||
|
intents_data = [
|
||||||
|
('Friendship', 'bi-people'),
|
||||||
|
('Networking', 'bi-briefcase'),
|
||||||
|
('Activity Partner', 'bi-bicycle'),
|
||||||
|
('Accountability', 'bi-check-circle')
|
||||||
|
]
|
||||||
|
for name, icon in intents_data:
|
||||||
|
Intent.objects.create(name=name, icon=icon)
|
||||||
|
|
||||||
|
if not Profile.objects.exists():
|
||||||
|
# Create a demo user/profile
|
||||||
|
demo_user, _ = User.objects.get_or_create(username='marcus_v', first_name='Marcus', last_name='V.')
|
||||||
|
p = Profile.objects.create(
|
||||||
|
user=demo_user,
|
||||||
|
professional_headline='Architect & Urban Planner',
|
||||||
|
transition_status='new-in-town',
|
||||||
|
bio='Passionate about sustainable cities. Recently moved here from Chicago and looking for local communities.',
|
||||||
|
location_city='Austin, TX',
|
||||||
|
)
|
||||||
|
p.intents.add(Intent.objects.get(name='Networking'))
|
||||||
|
|
||||||
|
demo_user2, _ = User.objects.get_or_create(username='sarah_l', first_name='Sarah', last_name='L.')
|
||||||
|
p2 = Profile.objects.create(
|
||||||
|
user=demo_user2,
|
||||||
|
professional_headline='UX Researcher | Growth Mindset',
|
||||||
|
transition_status='post-divorce',
|
||||||
|
bio='Rediscovering my love for hiking and photography. Seeking authentic connections and shared growth.',
|
||||||
|
location_city='Austin, TX',
|
||||||
|
)
|
||||||
|
p2.intents.add(Intent.objects.get(name='Friendship'))
|
||||||
|
|
||||||
context = {
|
# Social Feed Logic
|
||||||
"project_name": "New Style",
|
hidden_post_ids = []
|
||||||
"agent_brand": agent_brand,
|
if request.user.is_authenticated:
|
||||||
"django_version": django_version(),
|
hidden_post_ids = HiddenPost.objects.filter(user=request.user).values_list('post_id', flat=True)
|
||||||
"python_version": platform.python_version(),
|
|
||||||
"current_time": now,
|
posts = Post.objects.exclude(id__in=hidden_post_ids).select_related('author', 'author__profile').prefetch_related('comments', 'comments__author', 'reactions')
|
||||||
"host_name": host_name,
|
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
# Filtering by intent (for discovery)
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
intent_filter = request.GET.get('intent')
|
||||||
}
|
if intent_filter:
|
||||||
|
profiles = Profile.objects.filter(intents__name__iexact=intent_filter)
|
||||||
|
else:
|
||||||
|
profiles = Profile.objects.all()
|
||||||
|
|
||||||
|
intents = Intent.objects.all()
|
||||||
|
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
context.update({
|
||||||
|
"profiles": profiles,
|
||||||
|
"intents": intents,
|
||||||
|
"current_intent": intent_filter,
|
||||||
|
"posts": posts,
|
||||||
|
})
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def create_post(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
content = request.POST.get('content')
|
||||||
|
image = request.FILES.get('image')
|
||||||
|
post_type = request.POST.get('post_type', 'reflection')
|
||||||
|
if content or image:
|
||||||
|
Post.objects.create(author=request.user, content=content, image=image, post_type=post_type)
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def delete_post(request, post_id):
|
||||||
|
post = get_object_or_404(Post, id=post_id, author=request.user)
|
||||||
|
post.delete()
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_comment(request, post_id):
|
||||||
|
if request.method == 'POST':
|
||||||
|
post = get_object_or_404(Post, id=post_id)
|
||||||
|
content = request.POST.get('content')
|
||||||
|
if content:
|
||||||
|
Comment.objects.create(post=post, author=request.user, content=content)
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def toggle_reaction(request, post_id):
|
||||||
|
post = get_object_or_404(Post, id=post_id)
|
||||||
|
reaction_type = request.GET.get('type', 'heart')
|
||||||
|
reaction, created = Reaction.objects.get_or_create(
|
||||||
|
post=post, user=request.user, reaction_type=reaction_type
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
reaction.delete()
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def toggle_follow(request, username):
|
||||||
|
target_user = get_object_or_404(User, username=username)
|
||||||
|
if target_user == request.user:
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
follow, created = Follow.objects.get_or_create(follower=request.user, followed=target_user)
|
||||||
|
if not created:
|
||||||
|
follow.delete()
|
||||||
|
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def hide_post(request, post_id):
|
||||||
|
post = get_object_or_404(Post, id=post_id)
|
||||||
|
HiddenPost.objects.get_or_create(user=request.user, post=post)
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
def about(request):
|
||||||
|
return render(request, "core/about.html")
|
||||||
|
|
||||||
|
def signup(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = UserCreationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
# Create profile
|
||||||
|
Profile.objects.get_or_create(user=user)
|
||||||
|
login(request, user)
|
||||||
|
return redirect('onboarding')
|
||||||
|
else:
|
||||||
|
form = UserCreationForm()
|
||||||
|
return render(request, 'registration/signup.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def onboarding(request):
|
||||||
|
profile = request.user.profile
|
||||||
|
if request.method == 'POST':
|
||||||
|
profile.gamer_tag = request.POST.get('gamer_tag', '')
|
||||||
|
profile.platform = request.POST.get('platform', 'pc')
|
||||||
|
profile.primary_game_id = request.POST.get('primary_game')
|
||||||
|
profile.bio = request.POST.get('bio', '')
|
||||||
|
profile.onboarding_completed = True
|
||||||
|
profile.save()
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
from .models import Game
|
||||||
|
games = Game.objects.all().order_by('name')
|
||||||
|
return render(request, 'core/onboarding.html', {
|
||||||
|
'profile': profile,
|
||||||
|
'games': games,
|
||||||
|
'platforms': profile.PLATFORM_CHOICES
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def settings_view(request):
|
||||||
|
profile = request.user.profile
|
||||||
|
if request.method == 'POST':
|
||||||
|
profile.two_factor_enabled = 'two_factor' in request.POST
|
||||||
|
profile.save()
|
||||||
|
return redirect('settings')
|
||||||
|
return render(request, 'core/settings.html', {'profile': profile})
|
||||||
|
|
||||||
|
def get_started(request):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return redirect('signup')
|
||||||
|
if not request.user.profile.onboarding_completed:
|
||||||
|
return redirect('onboarding')
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def inbox(request):
|
||||||
|
# Get all users the current user has messaged or received messages from
|
||||||
|
sent_to = Message.objects.filter(sender=request.user).values_list('recipient', flat=True)
|
||||||
|
received_from = Message.objects.filter(recipient=request.user).values_list('sender', flat=True)
|
||||||
|
|
||||||
|
partner_ids = set(list(sent_to) + list(received_from))
|
||||||
|
partners = User.objects.filter(id__in=partner_ids).select_related('profile')
|
||||||
|
|
||||||
|
for partner in partners:
|
||||||
|
last_message = Message.objects.filter(
|
||||||
|
Q(sender=request.user, recipient=partner) |
|
||||||
|
Q(sender=partner, recipient=request.user)
|
||||||
|
).order_by('-timestamp').first()
|
||||||
|
partner.last_message = last_message
|
||||||
|
|
||||||
|
return render(request, 'core/inbox.html', {'partners': partners})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def chat_detail(request, username):
|
||||||
|
partner = get_object_or_404(User, username=username)
|
||||||
|
if partner == request.user:
|
||||||
|
return redirect('inbox')
|
||||||
|
|
||||||
|
thread = Thread.objects.filter(participants=request.user).filter(participants=partner).first()
|
||||||
|
if not thread:
|
||||||
|
thread = Thread.objects.create()
|
||||||
|
thread.participants.add(request.user, partner)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
body = request.POST.get('body')
|
||||||
|
if body:
|
||||||
|
Message.objects.create(sender=request.user, recipient=partner, body=body, thread=thread)
|
||||||
|
thread.updated_at = timezone.now()
|
||||||
|
thread.save()
|
||||||
|
return redirect('chat_detail', username=username)
|
||||||
|
|
||||||
|
messages = Message.objects.filter(thread=thread).order_by('timestamp')
|
||||||
|
messages.filter(recipient=request.user, is_read=False).update(is_read=True)
|
||||||
|
|
||||||
|
return render(request, 'core/chat.html', {
|
||||||
|
'partner': partner,
|
||||||
|
'chat_messages': messages,
|
||||||
|
'thread': thread
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request):
|
||||||
|
return redirect('profile_detail', username=request.user.username)
|
||||||
|
|
||||||
|
def profile_detail(request, username):
|
||||||
|
target_user = get_object_or_404(User, username=username)
|
||||||
|
is_following = False
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
is_following = Follow.objects.filter(follower=request.user, followed=target_user).exists()
|
||||||
|
return render(request, 'core/profile_detail.html', {
|
||||||
|
'target_user': target_user,
|
||||||
|
'is_following': is_following
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def edit_profile(request):
|
||||||
|
profile = request.user.profile
|
||||||
|
if request.method == 'POST':
|
||||||
|
profile.professional_headline = request.POST.get('headline', '')
|
||||||
|
profile.bio = request.POST.get('bio', '')
|
||||||
|
profile.location_city = request.POST.get('location', '')
|
||||||
|
profile.platform = request.POST.get('platform', '')
|
||||||
|
profile.gamer_tag = request.POST.get('gamer_tag', '')
|
||||||
|
profile.rank = request.POST.get('rank', '')
|
||||||
|
profile.preferred_role = request.POST.get('preferred_role', '')
|
||||||
|
|
||||||
|
primary_game_id = request.POST.get('primary_game')
|
||||||
|
if primary_game_id:
|
||||||
|
profile.primary_game_id = primary_game_id
|
||||||
|
|
||||||
|
if 'avatar' in request.FILES:
|
||||||
|
profile.avatar = request.FILES['avatar']
|
||||||
|
profile.save()
|
||||||
|
return redirect('my_profile')
|
||||||
|
|
||||||
|
from .models import Game
|
||||||
|
games = Game.objects.all().order_by('name')
|
||||||
|
return render(request, 'core/edit_profile.html', {
|
||||||
|
'profile': profile,
|
||||||
|
'games': games,
|
||||||
|
'platforms': profile.PLATFORM_CHOICES
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def matches(request, tab=None):
|
||||||
|
"""Main hub for 'My Matches'."""
|
||||||
|
if not tab:
|
||||||
|
tab = request.GET.get('tab', 'mutual')
|
||||||
|
search_query = request.GET.get('q', '')
|
||||||
|
intent_filter = request.GET.get('intent', '')
|
||||||
|
sort_by = request.GET.get('sort', 'newest')
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if tab == 'requests':
|
||||||
|
queryset = User.objects.filter(requests_sent__to_user=user, requests_sent__status='pending')
|
||||||
|
elif tab == 'sent':
|
||||||
|
queryset = User.objects.filter(requests_received__from_user=user, requests_received__status='pending')
|
||||||
|
elif tab == 'liked':
|
||||||
|
queryset = User.objects.filter(likes_received__from_user=user).exclude(
|
||||||
|
Q(matches_a__user_b=user) | Q(matches_b__user_a=user)
|
||||||
|
)
|
||||||
|
elif tab == 'blocked':
|
||||||
|
queryset = User.objects.filter(blocks_received__blocker=user)
|
||||||
|
else: # mutual
|
||||||
|
queryset = User.objects.filter(
|
||||||
|
Q(matches_a__user_b=user, matches_a__status='active') |
|
||||||
|
Q(matches_b__user_a=user, matches_b__status='active')
|
||||||
|
)
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(first_name__icontains=search_query) |
|
||||||
|
Q(last_name__icontains=search_query) |
|
||||||
|
Q(username__icontains=search_query) |
|
||||||
|
Q(profile__professional_headline__icontains=search_query) |
|
||||||
|
Q(profile__location_city__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
if intent_filter:
|
||||||
|
queryset = queryset.filter(profile__intents__name=intent_filter)
|
||||||
|
|
||||||
|
if sort_by == 'newest':
|
||||||
|
queryset = queryset.order_by('-date_joined')
|
||||||
|
elif sort_by == 'aligned':
|
||||||
|
queryset = queryset.order_by('-profile__accountability_streak')
|
||||||
|
|
||||||
|
queryset = queryset.select_related('profile').prefetch_related('profile__intents').distinct()
|
||||||
|
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
incoming_requests = ConnectionRequest.objects.filter(to_user=user, status='pending')
|
||||||
|
request_map = {r.from_user_id: r.id for r in incoming_requests}
|
||||||
|
|
||||||
|
context.update({
|
||||||
|
'matches_list': queryset,
|
||||||
|
'current_tab': tab,
|
||||||
|
'search_query': search_query,
|
||||||
|
'current_intent': intent_filter,
|
||||||
|
'current_sort': sort_by,
|
||||||
|
'intents': Intent.objects.all(),
|
||||||
|
'title': 'My Matches',
|
||||||
|
'request_map': request_map
|
||||||
|
})
|
||||||
|
return render(request, 'core/matches.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def send_match_request(request, username):
|
||||||
|
target_user = get_object_or_404(User, username=username)
|
||||||
|
if target_user == request.user:
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
if Like.objects.filter(from_user=target_user, to_user=request.user).exists():
|
||||||
|
Match.objects.get_or_create(
|
||||||
|
user_a=min(request.user, target_user, key=lambda u: u.id),
|
||||||
|
user_b=max(request.user, target_user, key=lambda u: u.id)
|
||||||
|
)
|
||||||
|
return redirect('matches')
|
||||||
|
|
||||||
|
ConnectionRequest.objects.get_or_create(from_user=request.user, to_user=target_user, status='pending')
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def handle_match_request(request, request_id):
|
||||||
|
conn_request = get_object_or_404(ConnectionRequest, id=request_id, to_user=request.user)
|
||||||
|
action = request.GET.get('action')
|
||||||
|
|
||||||
|
if action == 'accept':
|
||||||
|
conn_request.status = 'accepted'
|
||||||
|
conn_request.responded_at = timezone.now()
|
||||||
|
conn_request.save()
|
||||||
|
|
||||||
|
Match.objects.get_or_create(
|
||||||
|
user_a=min(conn_request.from_user, conn_request.to_user, key=lambda u: u.id),
|
||||||
|
user_b=max(conn_request.from_user, conn_request.to_user, key=lambda u: u.id)
|
||||||
|
)
|
||||||
|
elif action == 'decline':
|
||||||
|
conn_request.status = 'declined'
|
||||||
|
conn_request.responded_at = timezone.now()
|
||||||
|
conn_request.save()
|
||||||
|
|
||||||
|
return redirect('matches')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def cancel_match_request(request, username):
|
||||||
|
target_user = get_object_or_404(User, username=username)
|
||||||
|
ConnectionRequest.objects.filter(from_user=request.user, to_user=target_user, status='pending').delete()
|
||||||
|
return redirect('matches')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def block_user(request, username):
|
||||||
|
target_user = get_object_or_404(User, username=username)
|
||||||
|
Block.objects.get_or_create(blocker=request.user, blocked=target_user)
|
||||||
|
Match.objects.filter(
|
||||||
|
Q(user_a=request.user, user_b=target_user) |
|
||||||
|
Q(user_a=target_user, user_b=request.user)
|
||||||
|
).delete()
|
||||||
|
ConnectionRequest.objects.filter(
|
||||||
|
Q(from_user=request.user, to_user=target_user) |
|
||||||
|
Q(from_user=target_user, to_user=request.user)
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return redirect('matches')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def toggle_like(request, username):
|
||||||
|
target_user = get_object_or_404(User, username=username)
|
||||||
|
if target_user == request.user:
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
like, created = Like.objects.get_or_create(from_user=request.user, to_user=target_user)
|
||||||
|
if not created:
|
||||||
|
like.delete()
|
||||||
|
else:
|
||||||
|
if Like.objects.filter(from_user=target_user, to_user=request.user).exists():
|
||||||
|
Match.objects.get_or_create(
|
||||||
|
user_a=min(request.user, target_user, key=lambda u: u.id),
|
||||||
|
user_b=max(request.user, target_user, key=lambda u: u.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def groups(request):
|
||||||
|
return render(request, 'core/placeholder.html', {'title': 'Groups'})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def my_posts(request):
|
||||||
|
posts = Post.objects.filter(author=request.user).select_related('author', 'author__profile').prefetch_related('comments', 'reactions')
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
context.update({
|
||||||
|
'posts': posts,
|
||||||
|
'title': 'My Posts'
|
||||||
|
})
|
||||||
|
return render(request, 'core/index.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def events(request):
|
||||||
|
tab = request.GET.get('tab', 'upcoming')
|
||||||
|
query = request.GET.get('q', '')
|
||||||
|
|
||||||
|
events = Event.objects.all().order_by('start_datetime')
|
||||||
|
|
||||||
|
if query:
|
||||||
|
events = events.filter(
|
||||||
|
Q(title__icontains=query) | Q(description__icontains=query) | Q(location_name__icontains=query)
|
||||||
|
)
|
||||||
|
|
||||||
|
if tab == 'mine':
|
||||||
|
events = events.filter(creator=request.user)
|
||||||
|
elif tab == 'attending':
|
||||||
|
events = events.filter(rsvps__user=request.user, rsvps__status='going')
|
||||||
|
elif tab == 'past':
|
||||||
|
events = Event.objects.filter(start_datetime__lt=timezone.now()).order_by('-start_datetime')
|
||||||
|
elif tab == 'calendar':
|
||||||
|
# Simple grouping by date for calendar view
|
||||||
|
events = events.filter(start_datetime__gt=timezone.now())
|
||||||
|
else: # upcoming
|
||||||
|
events = events.filter(start_datetime__gt=timezone.now())
|
||||||
|
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
|
||||||
|
# For calendar view, group events by date
|
||||||
|
calendar_events = {}
|
||||||
|
if tab == 'calendar':
|
||||||
|
for event in events:
|
||||||
|
date_key = event.start_datetime.date()
|
||||||
|
if date_key not in calendar_events:
|
||||||
|
calendar_events[date_key] = []
|
||||||
|
calendar_events[date_key].append(event)
|
||||||
|
|
||||||
|
context.update({
|
||||||
|
'events': events,
|
||||||
|
'calendar_events': calendar_events,
|
||||||
|
'tab': tab,
|
||||||
|
'query': query,
|
||||||
|
})
|
||||||
|
return render(request, 'core/events_list.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def event_detail(request, event_id):
|
||||||
|
event = get_object_or_404(Event, id=event_id)
|
||||||
|
user_rsvp = RSVP.objects.filter(event=event, user=request.user).first()
|
||||||
|
rsvps = event.rsvps.select_related('user', 'user__profile')
|
||||||
|
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
context.update({
|
||||||
|
'event': event,
|
||||||
|
'user_rsvp': user_rsvp,
|
||||||
|
'rsvps': rsvps,
|
||||||
|
'going_count': rsvps.filter(status='going').count(),
|
||||||
|
'maybe_count': rsvps.filter(status='maybe').count(),
|
||||||
|
})
|
||||||
|
return render(request, 'core/event_detail.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def event_create(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = EventForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
event = form.save(commit=False)
|
||||||
|
event.creator = request.user
|
||||||
|
event.save()
|
||||||
|
form.save_m2m()
|
||||||
|
return redirect('event_detail', event_id=event.id)
|
||||||
|
else:
|
||||||
|
form = EventForm()
|
||||||
|
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
context.update({'form': form, 'title': 'Create Session'})
|
||||||
|
return render(request, 'core/event_form.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def event_edit(request, event_id):
|
||||||
|
event = get_object_or_404(Event, id=event_id, creator=request.user)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = EventForm(request.POST, request.FILES, instance=event)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect('event_detail', event_id=event.id)
|
||||||
|
else:
|
||||||
|
form = EventForm(instance=event)
|
||||||
|
|
||||||
|
context = get_dashboard_context(request)
|
||||||
|
context.update({'form': form, 'title': 'Edit Session', 'event': event})
|
||||||
|
return render(request, 'core/event_form.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def event_delete(request, event_id):
|
||||||
|
event = get_object_or_404(Event, id=event_id, creator=request.user)
|
||||||
|
if request.method == 'POST':
|
||||||
|
event.delete()
|
||||||
|
return redirect('events')
|
||||||
|
return render(request, 'core/event_confirm_delete.html', {'event': event})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def event_rsvp(request, event_id):
|
||||||
|
if request.method == 'POST':
|
||||||
|
event = get_object_or_404(Event, id=event_id)
|
||||||
|
status = request.POST.get('status')
|
||||||
|
if status in ['going', 'maybe', 'not_going']:
|
||||||
|
rsvp, created = RSVP.objects.update_or_create(
|
||||||
|
event=event, user=request.user,
|
||||||
|
defaults={'status': status}
|
||||||
|
)
|
||||||
|
elif status == 'cancel':
|
||||||
|
RSVP.objects.filter(event=event, user=request.user).delete()
|
||||||
|
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'event_detail', event_id=event.id))
|
||||||
|
return redirect('events')
|
||||||
|
|||||||
0
groups/__init__.py
Normal file
BIN
groups/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
groups/__pycache__/admin.cpython-311.pyc
Normal file
BIN
groups/__pycache__/apps.cpython-311.pyc
Normal file
BIN
groups/__pycache__/models.cpython-311.pyc
Normal file
BIN
groups/__pycache__/urls.cpython-311.pyc
Normal file
BIN
groups/__pycache__/views.cpython-311.pyc
Normal file
3
groups/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
groups/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GroupsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'groups'
|
||||||
51
groups/migrations/0001_initial.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 18:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('group_type', models.CharField(choices=[('circle', 'Circle (4–12 members)'), ('community', 'Community (Open)'), ('mastermind', 'Mastermind (Structured Recurring)'), ('event_based', 'Event Based'), ('private_invite', 'Private Invite')], max_length=50)),
|
||||||
|
('intent_type', models.CharField(choices=[('friendship', 'Friendship'), ('networking', 'Networking'), ('activity', 'Activity'), ('accountability', 'Accountability')], max_length=50)),
|
||||||
|
('transition_focus', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('visibility', models.CharField(choices=[('public_members', 'Public (Members Only)'), ('private_request', 'Private (Request to Join)'), ('invite_only', 'Invite Only')], default='public_members', max_length=50)),
|
||||||
|
('capacity', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('location_scope', models.CharField(choices=[('local', 'Local'), ('virtual', 'Virtual')], default='local', max_length=20)),
|
||||||
|
('city', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('state', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('recurring_schedule', models.TextField(blank=True, help_text='Required for Mastermind groups. Use structured text or JSON.', null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('cover_image', models.ImageField(blank=True, null=True, upload_to='groups/covers/')),
|
||||||
|
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_groups_v2', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GroupMember',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('role', models.CharField(choices=[('member', 'Member'), ('moderator', 'Moderator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||||
|
('status', models.CharField(choices=[('active', 'Active'), ('pending', 'Pending')], default='active', max_length=20)),
|
||||||
|
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='groups.group')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_memberships', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('group', 'user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 18:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('groups', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='group_type',
|
||||||
|
field=models.CharField(choices=[('squad', 'Squad (4–12 members)'), ('community', 'Community (Open)'), ('mastermind', 'Pro Squad (Structured)'), ('tournament', 'Tournament Team'), ('private', 'Private Invite')], max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='intent_type',
|
||||||
|
field=models.CharField(choices=[('ranked', 'Ranked Squad'), ('duo', 'Duo'), ('casual', 'Casual'), ('tournament', 'Tournament'), ('practice', 'Practice')], max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-17 18:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('groups', '0002_alter_group_group_type_alter_group_intent_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='group',
|
||||||
|
old_name='transition_focus',
|
||||||
|
new_name='focus_game',
|
||||||
|
),
|
||||||
|
]
|
||||||
0
groups/migrations/__init__.py
Normal file
BIN
groups/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
groups/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
100
groups/models.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
class Group(models.Model):
|
||||||
|
GROUP_TYPE_CHOICES = [
|
||||||
|
('squad', 'Squad (4–12 members)'),
|
||||||
|
('community', 'Community (Open)'),
|
||||||
|
('mastermind', 'Pro Squad (Structured)'),
|
||||||
|
('tournament', 'Tournament Team'),
|
||||||
|
('private', 'Private Invite'),
|
||||||
|
]
|
||||||
|
|
||||||
|
INTENT_TYPE_CHOICES = [
|
||||||
|
('ranked', 'Ranked Squad'),
|
||||||
|
('duo', 'Duo'),
|
||||||
|
('casual', 'Casual'),
|
||||||
|
('tournament', 'Tournament'),
|
||||||
|
('practice', 'Practice'),
|
||||||
|
]
|
||||||
|
|
||||||
|
VISIBILITY_CHOICES = [
|
||||||
|
('public_members', 'Public (Members Only)'),
|
||||||
|
('private_request', 'Private (Request to Join)'),
|
||||||
|
('invite_only', 'Invite Only'),
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCATION_SCOPE_CHOICES = [
|
||||||
|
('local', 'Local'),
|
||||||
|
('virtual', 'Virtual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField()
|
||||||
|
group_type = models.CharField(max_length=50, choices=GROUP_TYPE_CHOICES)
|
||||||
|
intent_type = models.CharField(max_length=50, choices=INTENT_TYPE_CHOICES)
|
||||||
|
focus_game = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
visibility = models.CharField(max_length=50, choices=VISIBILITY_CHOICES, default='public_members')
|
||||||
|
capacity = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
location_scope = models.CharField(max_length=20, choices=LOCATION_SCOPE_CHOICES, default='local')
|
||||||
|
city = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
state = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
recurring_schedule = models.TextField(blank=True, null=True, help_text="Required for Mastermind groups. Use structured text or JSON.")
|
||||||
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_groups_v2')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
cover_image = models.ImageField(upload_to='groups/covers/', blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.group_type == 'circle':
|
||||||
|
if self.capacity and self.capacity > 12:
|
||||||
|
raise ValidationError({'capacity': 'Circles cannot have more than 12 members.'})
|
||||||
|
if not self.capacity:
|
||||||
|
self.capacity = 12
|
||||||
|
|
||||||
|
if self.group_type == 'mastermind' and not self.recurring_schedule:
|
||||||
|
raise ValidationError({'recurring_schedule': 'Mastermind groups require a structured schedule.'})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.full_clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class GroupMember(models.Model):
|
||||||
|
ROLE_CHOICES = [
|
||||||
|
('member', 'Member'),
|
||||||
|
('moderator', 'Moderator'),
|
||||||
|
('owner', 'Owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('active', 'Active'),
|
||||||
|
('pending', 'Pending'),
|
||||||
|
]
|
||||||
|
|
||||||
|
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name='memberships')
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='group_memberships')
|
||||||
|
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='member')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||||
|
joined_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('group', 'user')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} in {self.group.name}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.full_clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.status == 'active':
|
||||||
|
# Use filter().count() instead of exclude() for new objects
|
||||||
|
current_count = GroupMember.objects.filter(group=self.group, status='active').exclude(id=self.id).count()
|
||||||
|
if self.group.capacity and current_count >= self.group.capacity:
|
||||||
|
raise ValidationError('This group has reached its maximum capacity.')
|
||||||
155
groups/templates/groups/group_detail.html
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ group.name }} | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid dashboard-container px-lg-5">
|
||||||
|
<div class="row">
|
||||||
|
<!-- LEFT SIDEBAR -->
|
||||||
|
<div class="col-lg-2 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link"><i class="bi bi-compass"></i> Teammates</a>
|
||||||
|
<a href="{% url 'groups:hub' %}" class="nav-link active"><i class="bi bi-grid"></i> Squads</a>
|
||||||
|
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||||
|
<a href="{% url 'events' %}" class="nav-link"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN COLUMN -->
|
||||||
|
<div class="col-lg-10 col-md-12">
|
||||||
|
<!-- Group Header Card -->
|
||||||
|
<div class="card border-0 shadow-soft mb-5 overflow-hidden" style="border-radius: 24px;">
|
||||||
|
{% if group.cover_image %}
|
||||||
|
<div style="height: 250px; background: url('{{ group.cover_image.url }}') center/cover no-repeat;"></div>
|
||||||
|
{% else %}
|
||||||
|
<div style="height: 150px; background: linear-gradient(135deg, #F6F3EC 0%, #E5E1D8 100%);"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card-body p-4 p-lg-5">
|
||||||
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||||
|
<span class="post-badge">{{ group.get_group_type_display }}</span>
|
||||||
|
<span class="intent-badge">{{ group.get_intent_type_display }}</span>
|
||||||
|
<span class="text-muted small ms-2"><i class="bi bi-geo-alt me-1"></i> {{ group.get_location_scope_display }}</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="display-5 fw-bold mb-2">{{ group.name }}</h1>
|
||||||
|
<p class="text-muted lead mb-0">{{ group.focus_game|default:"Competitive Gaming & Teamwork" }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
{% if is_member %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-cg px-4 dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
Member
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu border-0 shadow-sm p-2">
|
||||||
|
<li><a class="dropdown-item rounded small" href="{% url 'groups:members' group.pk %}">View Members</a></li>
|
||||||
|
{% if membership.role == 'owner' or membership.role == 'moderator' %}
|
||||||
|
<li><a class="dropdown-item rounded small" href="{% url 'groups:edit' group.pk %}">Settings</a></li>
|
||||||
|
<li><a class="dropdown-item rounded small" href="{% url 'groups:moderation' group.pk %}">Moderation</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<form action="{% url 'groups:leave' group.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="dropdown-item rounded small text-danger">Leave Squad</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% elif is_pending %}
|
||||||
|
<button class="btn btn-outline-cg px-4 disabled">Request Sent</button>
|
||||||
|
{% else %}
|
||||||
|
<form action="{% url 'groups:join' group.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-primary-cg px-5">Join Squad</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-5">
|
||||||
|
<!-- Content Column -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h2 class="section-header mb-4">About</h2>
|
||||||
|
<div class="bg-white p-4 p-lg-5 rounded-cg shadow-soft">
|
||||||
|
<p class="mb-0" style="white-space: pre-wrap; line-height: 1.8;">{{ group.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if group.group_type == 'mastermind' %}
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h2 class="section-header mb-4">Practice Schedule</h2>
|
||||||
|
<div class="card border-0 bg-secondary-section p-4" style="border-radius: 16px;">
|
||||||
|
<div class="d-flex gap-4">
|
||||||
|
<div class="stat-icon bg-white text-cg-slate">
|
||||||
|
<i class="bi bi-calendar-check fs-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-bold">Team Commitment</p>
|
||||||
|
<p class="text-muted small mb-0">{{ group.recurring_schedule }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Feed Placeholder -->
|
||||||
|
<div class="dashboard-section mt-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="section-header mb-0">Squad Feed</h2>
|
||||||
|
<button class="btn btn-outline-cg btn-sm border-0 bg-light px-3">Latest First</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||||
|
<i class="bi bi-chat-dots fs-1 text-muted opacity-25 mb-3 d-block"></i>
|
||||||
|
<p class="text-muted mb-0">The conversation is just beginning. Be the first to coordinate.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Column -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="stat-widget mb-5">
|
||||||
|
<h2 class="section-header mb-4" style="font-size: 1.1rem;">Squad Info</h2>
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<div class="d-flex justify-content-between small border-bottom pb-2">
|
||||||
|
<span class="text-muted">Visibility</span>
|
||||||
|
<span class="fw-bold">{{ group.get_visibility_display }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between small border-bottom pb-2">
|
||||||
|
<span class="text-muted">Capacity</span>
|
||||||
|
<span class="fw-bold">{{ active_members.count }} / {{ group.capacity|default:"∞" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between small border-bottom pb-2">
|
||||||
|
<span class="text-muted">Created</span>
|
||||||
|
<span class="fw-bold">{{ group.created_at|date:"M Y" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-widget">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="section-header mb-0" style="font-size: 1.1rem;">Members</h2>
|
||||||
|
<a href="{% url 'groups:members' group.pk %}" class="extra-small text-decoration-none fw-bold">View All</a>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for member in active_members|slice:":12" %}
|
||||||
|
<a href="{% url 'profile_detail' member.user.username %}" title="{{ member.user.username }}">
|
||||||
|
<img src="{{ member.user.profile.get_avatar_url }}" class="rounded-circle border" width="40" height="40" style="object-fit: cover;">
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
117
groups/templates/groups/group_form.html
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% if object %}Edit{% else %}Create{% endif %} Squad | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="dashboard-section text-center mb-5">
|
||||||
|
<h1 class="welcome-header">{% if object %}Refine your Squad{% else %}Start a new Squad{% endif %}</h1>
|
||||||
|
<p class="text-muted">Set the coordination and goals for your team.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-soft p-4 p-lg-5" style="border-radius: 24px;">
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-bold small">Squad Name</label>
|
||||||
|
<input type="text" name="name" class="form-control px-4 py-3 bg-light border-0" value="{{ form.name.value|default:'' }}" required placeholder="e.g. Apex Predators: Ranked Climb">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-bold small">Description & Goals</label>
|
||||||
|
<textarea name="description" class="form-control px-4 py-3 bg-light border-0" rows="5" required placeholder="What are the goals of this squad? Who are you looking for?">{{ form.description.value|default:'' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold small">Squad Type</label>
|
||||||
|
<select name="group_type" class="form-select px-4 py-3 bg-light border-0" required id="id_group_type">
|
||||||
|
{% for code, label in form.fields.group_type.choices %}
|
||||||
|
<option value="{{ code }}" {% if form.group_type.value == code %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold small">Intent Type</label>
|
||||||
|
<select name="intent_type" class="form-select px-4 py-3 bg-light border-0" required>
|
||||||
|
{% for code, label in form.fields.intent_type.choices %}
|
||||||
|
<option value="{{ code }}" {% if form.intent_type.value == code %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-bold small">Focus Game / Title (Optional)</label>
|
||||||
|
<input type="text" name="focus_game" class="form-control px-4 py-3 bg-light border-0" value="{{ form.focus_game.value|default:'' }}" placeholder="e.g. Apex Legends, Valorant, Tournament Prep">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold small">Visibility</label>
|
||||||
|
<select name="visibility" class="form-select px-4 py-3 bg-light border-0" required>
|
||||||
|
{% for code, label in form.fields.visibility.choices %}
|
||||||
|
<option value="{{ code }}" {% if form.visibility.value == code %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold small">Capacity (Optional)</label>
|
||||||
|
<input type="number" name="capacity" class="form-control px-4 py-3 bg-light border-0" value="{{ form.capacity.value|default:'' }}" placeholder="e.g. 5">
|
||||||
|
<small class="text-muted extra-small">Small Squads are capped at 12 by default.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12" id="mastermind_schedule_div" style="display: none;">
|
||||||
|
<label class="form-label fw-bold small">Practice Schedule (Required for Pro Squads)</label>
|
||||||
|
<textarea name="recurring_schedule" class="form-control px-4 py-3 bg-light border-0" rows="2" placeholder="e.g. Every Mon/Wed/Fri at 8 PM EST">{{ form.recurring_schedule.value|default:'' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold small">Location Scope</label>
|
||||||
|
<select name="location_scope" class="form-select px-4 py-3 bg-light border-0" required>
|
||||||
|
{% for code, label in form.fields.location_scope.choices %}
|
||||||
|
<option value="{{ code }}" {% if form.location_scope.value == code %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold small">Cover Image</label>
|
||||||
|
<input type="file" name="cover_image" class="form-control px-4 py-3 bg-light border-0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 pt-4">
|
||||||
|
<div class="d-flex gap-3 justify-content-center">
|
||||||
|
<a href="{% url 'groups:hub' %}" class="btn btn-outline-cg px-5 py-3">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary-cg px-5 py-3">{% if object %}Save Changes{% else %}Launch Squad{% endif %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeSelect = document.getElementById('id_group_type');
|
||||||
|
const scheduleDiv = document.getElementById('mastermind_schedule_div');
|
||||||
|
|
||||||
|
function toggleSchedule() {
|
||||||
|
if (typeSelect.value === 'mastermind') {
|
||||||
|
scheduleDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
scheduleDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelect.addEventListener('change', toggleSchedule);
|
||||||
|
toggleSchedule(); // Initial state
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
38
groups/templates/groups/group_members.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Members | {{ group.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="dashboard-section mb-5">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'groups:hub' %}" class="text-decoration-none">Groups</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'groups:detail' group.pk %}" class="text-decoration-none">{{ group.name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Members</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1 class="welcome-header">Community Members</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for membership in group.memberships.all %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card border-0 shadow-soft p-4" style="border-radius: 16px;">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img src="{{ membership.user.profile.get_avatar_url }}" class="rounded-circle me-3" width="64" height="64" style="object-fit: cover;">
|
||||||
|
<div>
|
||||||
|
<h3 class="h6 mb-1 fw-bold">{{ membership.user.get_full_name|default:membership.user.username }}</h3>
|
||||||
|
<p class="extra-small text-muted mb-2">{{ membership.user.profile.professional_headline|default:"Member" }}</p>
|
||||||
|
<span class="badge {% if membership.role == 'owner' %}bg- cg-slate{% else %}bg-light text-muted{% endif %} extra-small rounded-pill">
|
||||||
|
{{ membership.get_role_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
176
groups/templates/groups/hub.html
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Squads Hub | Roster{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid dashboard-container px-lg-5">
|
||||||
|
<div class="row">
|
||||||
|
<!-- LEFT SIDEBAR -->
|
||||||
|
<div class="col-lg-2 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link"><i class="bi bi-compass"></i> Teammates</a>
|
||||||
|
<a href="{% url 'matches' %}" class="nav-link"><i class="bi bi-people"></i> My Teammates</a>
|
||||||
|
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||||
|
<a href="{% url 'events' %}" class="nav-link"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||||
|
<a href="{% url 'groups:hub' %}" class="nav-link active"><i class="bi bi-grid"></i> Squads</a>
|
||||||
|
<a href="{% url 'my_profile' %}" class="nav-link"><i class="bi bi-person"></i> Profile</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CENTER COLUMN -->
|
||||||
|
<div class="col-lg-7 col-md-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="dashboard-section pt-2 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="welcome-header mb-1">Squads</h1>
|
||||||
|
<p class="text-muted small">Intentional communities for gamers.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'groups:create' %}" class="btn btn-primary-cg px-4">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i> Create Squad
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- YOUR CIRCLES -->
|
||||||
|
{% if your_circles %}
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h2 class="section-header mb-4">Your Squads</h2>
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for group in your_circles %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0" style="border-radius: 16px;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<span class="post-badge">{{ group.get_group_type_display }}</span>
|
||||||
|
<span class="text-muted small"><i class="bi bi-people me-1"></i> {{ group.memberships.filter.status_active_count|default:group.memberships.count }}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-bold mb-2">{{ group.name }}</h3>
|
||||||
|
<p class="text-muted small line-clamp-2 mb-4">{{ group.description }}</p>
|
||||||
|
|
||||||
|
{% if group.group_type == 'mastermind' and group.recurring_schedule %}
|
||||||
|
<div class="bg-light p-3 rounded mb-4">
|
||||||
|
<p class="extra-small fw-bold text-muted mb-1"><i class="bi bi-calendar-check me-2"></i> NEXT MEETING</p>
|
||||||
|
<p class="small mb-0">{{ group.recurring_schedule|truncatechars:50 }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{% url 'groups:detail' group.pk %}" class="btn btn-outline-cg btn-sm">Enter Space</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- RECOMMENDED FOR YOU -->
|
||||||
|
{% if recommended_groups %}
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="section-header mb-0">Recommended For You</h2>
|
||||||
|
</div>
|
||||||
|
<div class="horizontal-scroll">
|
||||||
|
{% for group in recommended_groups %}
|
||||||
|
<div class="suggestion-card" style="width: 280px; text-align: left; padding: 24px;">
|
||||||
|
<span class="intent-badge mb-3 d-inline-block">{{ group.get_intent_type_display }}</span>
|
||||||
|
<h6 class="fw-bold mb-2">{{ group.name }}</h6>
|
||||||
|
<p class="extra-small text-muted mb-4 line-clamp-2">{{ group.description }}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
||||||
|
<span class="extra-small text-muted">{{ group.memberships.count }} members</span>
|
||||||
|
<a href="{% url 'groups:detail' group.pk %}" class="btn btn-primary-cg btn-sm px-3">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- DISCOVER GROUPS -->
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="section-header mb-0">Discover Squads</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search & Filters -->
|
||||||
|
<div class="card p-3 mb-4 border-0 shadow-soft">
|
||||||
|
<form method="GET" class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-white border-0"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" name="q" class="form-control border-0 bg-white" placeholder="Search by name or focus...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<select name="type" class="form-select border-0 bg-light">
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="squad">Squad</option>
|
||||||
|
<option value="community">Community</option>
|
||||||
|
<option value="mastermind">Pro Squad</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="submit" class="btn btn-primary-cg w-100">Filter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for group in discover_groups %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0" style="border-radius: 16px;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<span class="post-badge">{{ group.get_group_type_display }}</span>
|
||||||
|
<span class="text-muted small"><i class="bi bi-geo-alt me-1"></i> {{ group.get_location_scope_display }}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-bold mb-2">{{ group.name }}</h3>
|
||||||
|
<p class="text-muted small line-clamp-2 mb-4">{{ group.description }}</p>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="members-overlap me-2">
|
||||||
|
<!-- Placeholder for member avatars -->
|
||||||
|
<div class="avatar-sm rounded-circle bg-light border d-inline-block" style="width: 24px; height: 24px;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="extra-small text-muted">{{ group.memberships.count }} members</span>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'groups:detail' group.pk %}" class="btn btn-outline-cg btn-sm">Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<p class="text-muted">No squads found. Be the first to start one!</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT SIDEBAR -->
|
||||||
|
<div class="col-lg-3 d-none d-lg-block">
|
||||||
|
<div class="sticky-sidebar pt-2">
|
||||||
|
<div class="stat-widget">
|
||||||
|
<h2 class="section-header mb-4" style="font-size: 1.1rem;">Squad Guidelines</h2>
|
||||||
|
<ul class="list-unstyled small text-muted">
|
||||||
|
<li class="mb-3"><i class="bi bi-check2-circle text-sage me-2"></i> <strong>Coordination:</strong> Squads are for active play and team coordination.</li>
|
||||||
|
<li class="mb-3"><i class="bi bi-check2-circle text-sage me-2"></i> <strong>Respect:</strong> Maintain a high-trust, toxicity-free environment.</li>
|
||||||
|
<li class="mb-3"><i class="bi bi-check2-circle text-sage me-2"></i> <strong>Participation:</strong> Pro Squads expect active presence and regular sessions.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-widget mt-4">
|
||||||
|
<h2 class="section-header mb-4" style="font-size: 1.1rem;">Starting a Squad?</h2>
|
||||||
|
<p class="small text-muted mb-4">Pro Squads require a structured schedule to ensure all members are aligned on practice times.</p>
|
||||||
|
<a href="{% url 'groups:create' %}" class="btn btn-primary-cg btn-sm w-100 py-2">Get Started</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
3
groups/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
15
groups/urls.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'groups'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.GroupsHubView.as_view(), name='hub'),
|
||||||
|
path('create/', views.GroupCreateView.as_view(), name='create'),
|
||||||
|
path('<int:pk>/', views.GroupDetailView.as_view(), name='detail'),
|
||||||
|
path('<int:pk>/edit/', views.GroupUpdateView.as_view(), name='edit'),
|
||||||
|
path('<int:pk>/members/', views.GroupMembersView.as_view(), name='members'),
|
||||||
|
path('<int:pk>/join/', views.GroupJoinView.as_view(), name='join'),
|
||||||
|
path('<int:pk>/leave/', views.GroupLeaveView.as_view(), name='leave'),
|
||||||
|
path('<int:pk>/moderation/', views.GroupModerationView.as_view(), name='moderation'),
|
||||||
|
]
|
||||||
123
groups/views.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.db.models import Q
|
||||||
|
from .models import Group, GroupMember
|
||||||
|
|
||||||
|
class GroupsHubView(LoginRequiredMixin, ListView):
|
||||||
|
model = Group
|
||||||
|
template_name = 'groups/hub.html'
|
||||||
|
context_object_name = 'discover_groups'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
context['your_circles'] = Group.objects.filter(
|
||||||
|
memberships__user=user,
|
||||||
|
memberships__status='active'
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# Simple recommendation based on intent (can be refined later)
|
||||||
|
user_profile = getattr(user, 'profile', None)
|
||||||
|
if user_profile:
|
||||||
|
user_intents = user_profile.intents.all()
|
||||||
|
context['recommended_groups'] = Group.objects.exclude(
|
||||||
|
memberships__user=user
|
||||||
|
).filter(
|
||||||
|
intent_type__in=[i.name.lower() for i in user_intents]
|
||||||
|
).distinct()[:3]
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
class GroupCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
model = Group
|
||||||
|
template_name = 'groups/group_form.html'
|
||||||
|
fields = [
|
||||||
|
'name', 'description', 'group_type', 'intent_type',
|
||||||
|
'focus_game', 'visibility', 'capacity',
|
||||||
|
'location_scope', 'city', 'state', 'recurring_schedule', 'cover_image'
|
||||||
|
]
|
||||||
|
success_url = reverse_lazy('groups:hub')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.created_by = self.request.user
|
||||||
|
response = super().form_valid(form)
|
||||||
|
# Automatically add creator as owner
|
||||||
|
GroupMember.objects.create(
|
||||||
|
group=self.object,
|
||||||
|
user=self.request.user,
|
||||||
|
role='owner',
|
||||||
|
status='active'
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
class GroupDetailView(LoginRequiredMixin, DetailView):
|
||||||
|
model = Group
|
||||||
|
template_name = 'groups/group_detail.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
context['is_member'] = self.object.memberships.filter(user=user, status='active').exists()
|
||||||
|
context['is_pending'] = self.object.memberships.filter(user=user, status='pending').exists()
|
||||||
|
context['membership'] = self.object.memberships.filter(user=user).first()
|
||||||
|
context['active_members'] = self.object.memberships.filter(status='active')
|
||||||
|
return context
|
||||||
|
|
||||||
|
class GroupUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
|
model = Group
|
||||||
|
template_name = 'groups/group_form.html'
|
||||||
|
fields = [
|
||||||
|
'name', 'description', 'group_type', 'intent_type',
|
||||||
|
'focus_game', 'visibility', 'capacity',
|
||||||
|
'location_scope', 'city', 'state', 'recurring_schedule', 'cover_image'
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Group.objects.filter(memberships__user=self.request.user, memberships__role__in=['owner', 'moderator'])
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('groups:detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
class GroupMembersView(LoginRequiredMixin, DetailView):
|
||||||
|
model = Group
|
||||||
|
template_name = 'groups/group_members.html'
|
||||||
|
|
||||||
|
class GroupJoinView(LoginRequiredMixin, View):
|
||||||
|
def post(self, request, pk):
|
||||||
|
group = get_object_or_404(Group, pk=pk)
|
||||||
|
|
||||||
|
# Check if already a member
|
||||||
|
if group.memberships.filter(user=request.user).exists():
|
||||||
|
return redirect('groups:detail', pk=pk)
|
||||||
|
|
||||||
|
status = 'active'
|
||||||
|
if group.visibility == 'private_request':
|
||||||
|
status = 'pending'
|
||||||
|
|
||||||
|
try:
|
||||||
|
GroupMember.objects.create(
|
||||||
|
group=group,
|
||||||
|
user=request.user,
|
||||||
|
status=status
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Handle capacity error from model clean
|
||||||
|
pass
|
||||||
|
|
||||||
|
return redirect('groups:detail', pk=pk)
|
||||||
|
|
||||||
|
class GroupLeaveView(LoginRequiredMixin, View):
|
||||||
|
def post(self, request, pk):
|
||||||
|
membership = get_object_or_404(GroupMember, group_id=pk, user=request.user)
|
||||||
|
if membership.role != 'owner':
|
||||||
|
membership.delete()
|
||||||
|
return redirect('groups:hub')
|
||||||
|
|
||||||
|
class GroupModerationView(LoginRequiredMixin, DetailView):
|
||||||
|
model = Group
|
||||||
|
template_name = 'groups/group_moderation.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Group.objects.filter(memberships__user=self.request.user, memberships__role__in=['owner', 'moderator'])
|
||||||
28
seed_games.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from core.models import Game
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
games = [
|
||||||
|
{"name": "League of Legends", "genre": "moba", "team_size": 5, "has_roles": True, "roles_json": ["Top", "Jungle", "Mid", "ADC", "Support"]},
|
||||||
|
{"name": "Valorant", "genre": "fps", "team_size": 5, "has_roles": True, "roles_json": ["Duelist", "Initiator", "Controller", "Sentinel"]},
|
||||||
|
{"name": "Counter-Strike 2", "genre": "fps", "team_size": 5, "has_roles": False},
|
||||||
|
{"name": "Apex Legends", "genre": "battle_royale", "team_size": 3, "has_roles": True, "roles_json": ["Offensive", "Defensive", "Support", "Recon"]},
|
||||||
|
{"name": "Overwatch 2", "genre": "fps", "team_size": 5, "has_roles": True, "roles_json": ["Tank", "Damage", "Support"]},
|
||||||
|
{"name": "Dota 2", "genre": "moba", "team_size": 5, "has_roles": True, "roles_json": ["Carry", "Mid", "Offlane", "Soft Support", "Hard Support"]},
|
||||||
|
{"name": "Fortnite", "genre": "battle_royale", "team_size": 4, "has_roles": False},
|
||||||
|
{"name": "Call of Duty: Warzone", "genre": "battle_royale", "team_size": 4, "has_roles": False},
|
||||||
|
{"name": "Rocket League", "genre": "sports", "team_size": 3, "has_roles": False},
|
||||||
|
{"name": "Minecraft", "genre": "other", "team_size": None, "has_roles": False},
|
||||||
|
]
|
||||||
|
|
||||||
|
for game_data in games:
|
||||||
|
Game.objects.get_or_create(
|
||||||
|
name=game_data["name"],
|
||||||
|
defaults={
|
||||||
|
"slug": slugify(game_data["name"]),
|
||||||
|
"genre": game_data["genre"],
|
||||||
|
"team_size": game_data.get("team_size"),
|
||||||
|
"has_roles": game_data["has_roles"],
|
||||||
|
"roles_json": game_data.get("roles_json"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"Successfully seeded {len(games)} games.")
|
||||||
@ -1,4 +1,172 @@
|
|||||||
/* Custom styles for the application */
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap');
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
:root {
|
||||||
|
--r-navy-deep: #050A18;
|
||||||
|
--r-navy-soft: #0C162D;
|
||||||
|
--r-cyan: #00B4DB;
|
||||||
|
--r-cyan-glow: rgba(0, 180, 219, 0.4);
|
||||||
|
--r-neon-green: #39FF14;
|
||||||
|
--r-neon-glow: rgba(57, 255, 20, 0.4);
|
||||||
|
--r-silver: #E0E0E0;
|
||||||
|
--r-silver-dim: #A0A0A0;
|
||||||
|
--r-glass-bg: rgba(255, 255, 255, 0.03);
|
||||||
|
--r-glass-border: rgba(224, 224, 224, 0.2);
|
||||||
|
--r-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: radial-gradient(circle at top center, #101B38 0%, var(--r-navy-deep) 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: var(--r-silver);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, .brand-font {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metallic Text Effect */
|
||||||
|
.text-metallic {
|
||||||
|
background: linear-gradient(135deg, #FFFFFF 0%, var(--r-silver) 50%, var(--r-silver-dim) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Navbar */
|
||||||
|
.navbar {
|
||||||
|
background: rgba(5, 10, 24, 0.85) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid var(--r-glass-border);
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--r-silver-dim) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover, .nav-link.active {
|
||||||
|
color: var(--r-cyan) !important;
|
||||||
|
text-shadow: 0 0 15px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Buttons */
|
||||||
|
.btn-neon {
|
||||||
|
background: linear-gradient(135deg, var(--r-cyan) 0%, #0083B0 100%);
|
||||||
|
color: white !important;
|
||||||
|
border: none;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.9rem 2.2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 8px 20px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 12px 30px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-cg {
|
||||||
|
border: 2px solid var(--r-silver-dim);
|
||||||
|
color: var(--r-silver);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-cg:hover {
|
||||||
|
border-color: var(--r-neon-green);
|
||||||
|
color: var(--r-neon-green);
|
||||||
|
box-shadow: 0 0 20px var(--r-neon-glow);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards & Panels */
|
||||||
|
.card, .profile-card, .glass-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.02) !important;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border: 1px solid var(--r-glass-border) !important;
|
||||||
|
border-radius: 28px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
color: var(--r-silver) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--r-cyan) !important;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
||||||
|
background: rgba(255, 255, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Title */
|
||||||
|
.hero-title {
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
background: linear-gradient(135deg, #FFFFFF 0%, var(--r-silver) 40%, var(--r-cyan) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styling */
|
||||||
|
.form-control, .form-select {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
border: 1px solid var(--r-glass-border) !important;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: white !important;
|
||||||
|
padding: 0.9rem 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: var(--r-cyan) !important;
|
||||||
|
box-shadow: 0 0 15px var(--r-cyan-glow);
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Nav Enhancement */
|
||||||
|
.bottom-nav {
|
||||||
|
background: rgba(5, 10, 24, 0.95);
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
border-top: 1px solid var(--r-glass-border);
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item {
|
||||||
|
color: var(--r-silver-dim) !important;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item.active {
|
||||||
|
color: var(--r-cyan) !important;
|
||||||
|
text-shadow: 0 0 10px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global Overrides */
|
||||||
|
.bg-light, .bg-white {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--r-silver-dim) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 10px; }
|
||||||
|
::-webkit-scrollbar-track { background: var(--r-navy-deep); }
|
||||||
|
::-webkit-scrollbar-thumb { background: #1C2541; border-radius: 5px; border: 2px solid var(--r-navy-deep); }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--r-cyan); }
|
||||||
|
|||||||
BIN
staticfiles/core/images/logo.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
@ -1,21 +1,172 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color-start: #6a11cb;
|
--r-navy-deep: #050A18;
|
||||||
--bg-color-end: #2575fc;
|
--r-navy-soft: #0C162D;
|
||||||
--text-color: #ffffff;
|
--r-cyan: #00B4DB;
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
--r-cyan-glow: rgba(0, 180, 219, 0.4);
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
--r-neon-green: #39FF14;
|
||||||
|
--r-neon-glow: rgba(57, 255, 20, 0.4);
|
||||||
|
--r-silver: #E0E0E0;
|
||||||
|
--r-silver-dim: #A0A0A0;
|
||||||
|
--r-glass-bg: rgba(255, 255, 255, 0.03);
|
||||||
|
--r-glass-border: rgba(224, 224, 224, 0.2);
|
||||||
|
--r-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
background: radial-gradient(circle at top center, #101B38 0%, var(--r-navy-deep) 100%);
|
||||||
font-family: 'Inter', sans-serif;
|
background-attachment: fixed;
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
color: var(--r-silver);
|
||||||
color: var(--text-color);
|
font-family: 'Inter', sans-serif;
|
||||||
display: flex;
|
min-height: 100vh;
|
||||||
justify-content: center;
|
margin: 0;
|
||||||
align-items: center;
|
padding: 0;
|
||||||
min-height: 100vh;
|
overflow-x: hidden;
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, .brand-font {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metallic Text Effect */
|
||||||
|
.text-metallic {
|
||||||
|
background: linear-gradient(135deg, #FFFFFF 0%, var(--r-silver) 50%, var(--r-silver-dim) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Navbar */
|
||||||
|
.navbar {
|
||||||
|
background: rgba(5, 10, 24, 0.85) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid var(--r-glass-border);
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--r-silver-dim) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover, .nav-link.active {
|
||||||
|
color: var(--r-cyan) !important;
|
||||||
|
text-shadow: 0 0 15px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Buttons */
|
||||||
|
.btn-neon {
|
||||||
|
background: linear-gradient(135deg, var(--r-cyan) 0%, #0083B0 100%);
|
||||||
|
color: white !important;
|
||||||
|
border: none;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.9rem 2.2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 8px 20px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 12px 30px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-cg {
|
||||||
|
border: 2px solid var(--r-silver-dim);
|
||||||
|
color: var(--r-silver);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-cg:hover {
|
||||||
|
border-color: var(--r-neon-green);
|
||||||
|
color: var(--r-neon-green);
|
||||||
|
box-shadow: 0 0 20px var(--r-neon-glow);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards & Panels */
|
||||||
|
.card, .profile-card, .glass-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.02) !important;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border: 1px solid var(--r-glass-border) !important;
|
||||||
|
border-radius: 28px;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
color: var(--r-silver) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--r-cyan) !important;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
||||||
|
background: rgba(255, 255, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Title */
|
||||||
|
.hero-title {
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
background: linear-gradient(135deg, #FFFFFF 0%, var(--r-silver) 40%, var(--r-cyan) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styling */
|
||||||
|
.form-control, .form-select {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
border: 1px solid var(--r-glass-border) !important;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: white !important;
|
||||||
|
padding: 0.9rem 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: var(--r-cyan) !important;
|
||||||
|
box-shadow: 0 0 15px var(--r-cyan-glow);
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Nav Enhancement */
|
||||||
|
.bottom-nav {
|
||||||
|
background: rgba(5, 10, 24, 0.95);
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
border-top: 1px solid var(--r-glass-border);
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item {
|
||||||
|
color: var(--r-silver-dim) !important;
|
||||||
|
transition: var(--r-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item.active {
|
||||||
|
color: var(--r-cyan) !important;
|
||||||
|
text-shadow: 0 0 10px var(--r-cyan-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global Overrides */
|
||||||
|
.bg-light, .bg-white {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--r-silver-dim) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 10px; }
|
||||||
|
::-webkit-scrollbar-track { background: var(--r-navy-deep); }
|
||||||
|
::-webkit-scrollbar-thumb { background: #1C2541; border-radius: 5px; border: 2px solid var(--r-navy-deep); }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--r-cyan); }
|
||||||
|
|||||||
BIN
staticfiles/pasted-20260217-172745-5275e704.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
staticfiles/pasted-20260217-184853-b9782cb3.png
Normal file
|
After Width: | Height: | Size: 434 KiB |