Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
d9aee95dfc Autosave: 20260217-190955 2026-02-17 19:09:56 +00:00
Flatlogic Bot
79a273568c Autosave: 20260217-184408 2026-02-17 18:44:08 +00:00
Flatlogic Bot
a08f77aa00 Autosave: 20260217-172745 2026-02-17 17:27:46 +00:00
104 changed files with 4532 additions and 189 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -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'

View File

@ -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)

Binary file not shown.

View 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
View 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'}),
}

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

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

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

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

View File

@ -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/'),
),
]

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

View File

@ -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')},
},
),
]

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

View File

@ -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')},
},
),
]

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

View File

@ -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(),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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')

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

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

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

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

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

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

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

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

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

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

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

View File

@ -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">
<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 %} {% endblock %}

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

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

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

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

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

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

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

View File

Binary file not shown.

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

View File

@ -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")),
] ]

View File

@ -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"
now = timezone.now()
context = { # Simple logic to seed data for the first run
"project_name": "New Style", if not Intent.objects.exists():
"agent_brand": agent_brand, intents_data = [
"django_version": django_version(), ('Friendship', 'bi-people'),
"python_version": platform.python_version(), ('Networking', 'bi-briefcase'),
"current_time": now, ('Activity Partner', 'bi-bicycle'),
"host_name": host_name, ('Accountability', 'bi-check-circle')
"project_description": os.getenv("PROJECT_DESCRIPTION", ""), ]
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), 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'))
# Social Feed Logic
hidden_post_ids = []
if request.user.is_authenticated:
hidden_post_ids = HiddenPost.objects.filter(user=request.user).values_list('post_id', flat=True)
posts = Post.objects.exclude(id__in=hidden_post_ids).select_related('author', 'author__profile').prefetch_related('comments', 'comments__author', 'reactions')
# Filtering by intent (for discovery)
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
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
groups/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
groups/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GroupsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'groups'

View 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 (412 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')},
},
),
]

View File

@ -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 (412 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),
),
]

View File

@ -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',
),
]

View File

Binary file not shown.

100
groups/models.py Normal file
View 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 (412 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.')

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

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

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

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

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
groups/urls.py Normal file
View 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
View 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
View 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.")

View File

@ -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); }

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -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); }

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Some files were not shown because too many files have changed in this diff Show More