Autosave: 20260217-184408
This commit is contained in:
parent
a08f77aa00
commit
79a273568c
BIN
assets/pasted-20260217-172745-5275e704.png
Normal file
BIN
assets/pasted-20260217-172745-5275e704.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
Binary file not shown.
Binary file not shown.
@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'core',
|
||||
'groups',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@ -21,6 +21,7 @@ from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("groups/", include("groups.urls")),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
|
||||
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35
core/forms.py
Normal file
35
core/forms.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django import forms
|
||||
from .models import Event, EventTag
|
||||
|
||||
class EventForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = [
|
||||
'title', 'description', 'start_datetime', 'end_datetime',
|
||||
'timezone', 'location_name', 'location_address', 'city', 'state',
|
||||
'is_online', 'online_url', 'visibility', 'group', 'capacity',
|
||||
'cover_image', 'tags'
|
||||
]
|
||||
labels = {
|
||||
'title': 'Session Title',
|
||||
'description': 'Session Description',
|
||||
'is_online': 'This is an online session',
|
||||
'group': 'Squad',
|
||||
}
|
||||
widgets = {
|
||||
'start_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
|
||||
'end_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'rows': 4, 'class': 'form-control'}),
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'timezone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'location_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'location_address': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'city': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'state': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'online_url': forms.URLInput(attrs={'class': 'form-control'}),
|
||||
'visibility': forms.Select(attrs={'class': 'form-select'}),
|
||||
'group': forms.Select(attrs={'class': 'form-select'}),
|
||||
'capacity': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'cover_image': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
'tags': forms.SelectMultiple(attrs={'class': 'form-select'}),
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_follow'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='recipient',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Thread',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('participants', models.ManyToManyField(related_name='threads', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='thread',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='core.thread'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Block',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('blocked', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks_received', to=settings.AUTH_USER_MODEL)),
|
||||
('blocker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks_given', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('blocker', 'blocked')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConnectionRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('declined', 'Declined'), ('canceled', 'Canceled'), ('blocked', 'Blocked')], default='pending', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('responded_at', models.DateTimeField(blank=True, null=True)),
|
||||
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests_sent', to=settings.AUTH_USER_MODEL)),
|
||||
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests_received', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('from_user', 'to_user')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Like',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes_given', to=settings.AUTH_USER_MODEL)),
|
||||
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes_received', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('from_user', 'to_user')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Match',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('archived', 'Archived')], default='active', max_length=20)),
|
||||
('user_a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches_a', to=settings.AUTH_USER_MODEL)),
|
||||
('user_b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches_b', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user_a', 'user_b')},
|
||||
},
|
||||
),
|
||||
]
|
||||
157
core/migrations/0010_eventtag_remove_event_attendees_and_more.py
Normal file
157
core/migrations/0010_eventtag_remove_event_attendees_and_more.py
Normal file
@ -0,0 +1,157 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_alter_message_recipient_thread_message_thread_block_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='attendees',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='end_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='organization_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='organizer',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='start_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='capacity',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='cover_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='events/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='creator',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='end_datetime',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='is_online',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='location_address',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='location_name',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='online_url',
|
||||
field=models.URLField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='start_datetime',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='timezone',
|
||||
field=models.CharField(default='UTC', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('public', 'Public (Members Only)'), ('group', 'Group Only'), ('invite', 'Invite Only')], default='public', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_events', to='core.group'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventInvite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('declined', 'Declined')], default='pending', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='core.event')),
|
||||
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites_sent', to=settings.AUTH_USER_MODEL)),
|
||||
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites_received', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, to='core.eventtag'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RSVP',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('going', 'Going'), ('maybe', 'Maybe'), ('not_going', 'Not Going')], max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rsvps', to='core.event')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rsvps', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('event', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_eventtag_remove_event_attendees_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='creator',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='location_name',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='start_datetime',
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,57 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 18:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_alter_event_creator_alter_event_location_name_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Game',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('slug', models.SlugField(max_length=255, unique=True)),
|
||||
('genre', models.CharField(choices=[('fps', 'FPS'), ('moba', 'MOBA'), ('battle_royale', 'Battle Royale'), ('sports', 'Sports'), ('mmo', 'MMO'), ('fighting', 'Fighting'), ('rpg', 'RPG'), ('strategy', 'Strategy'), ('other', 'Other')], max_length=50)),
|
||||
('team_size', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('has_roles', models.BooleanField(default=False)),
|
||||
('roles_json', models.JSONField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='gamer_tag',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='platform',
|
||||
field=models.CharField(blank=True, choices=[('pc', 'PC'), ('playstation', 'PlayStation'), ('xbox', 'Xbox'), ('nintendo', 'Nintendo Switch'), ('mobile', 'Mobile')], max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='preferred_role',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='rank',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='primary_game',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_players', to='core.game'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='secondary_games',
|
||||
field=models.ManyToManyField(blank=True, related_name='secondary_players', to='core.game'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 18:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_game_profile_gamer_tag_profile_platform_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='gamer_tag',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='platform',
|
||||
field=models.CharField(choices=[('pc', 'PC'), ('playstation', 'PlayStation'), ('xbox', 'Xbox'), ('nintendo', 'Nintendo Switch'), ('mobile', 'Mobile')], max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='primary_game',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_players', to='core.game'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
178
core/models.py
178
core/models.py
@ -14,7 +14,37 @@ class ValueTag(models.Model):
|
||||
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'),
|
||||
@ -32,6 +62,14 @@ class Profile(models.Model):
|
||||
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)
|
||||
|
||||
@ -51,7 +89,7 @@ class Profile(models.Model):
|
||||
|
||||
@property
|
||||
def connection_count(self):
|
||||
return Connection.objects.filter(models.Q(user1=self.user) | models.Q(user2=self.user)).count()
|
||||
return Match.objects.filter(models.Q(user_a=self.user) | models.Q(user_b=self.user)).count()
|
||||
|
||||
@property
|
||||
def following_count(self):
|
||||
@ -63,17 +101,18 @@ class Profile(models.Model):
|
||||
|
||||
@property
|
||||
def events_attended_count(self):
|
||||
return self.user.attending_events.count()
|
||||
return self.user.rsvps.filter(status='going').count()
|
||||
|
||||
@property
|
||||
def profile_completion_percentage(self):
|
||||
steps = 0
|
||||
total_steps = 5
|
||||
if self.professional_headline: steps += 1
|
||||
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):
|
||||
@ -112,20 +151,78 @@ class Group(models.Model):
|
||||
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_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
location = models.CharField(max_length=255)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name='events', null=True, blank=True)
|
||||
organizer = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
attendees = models.ManyToManyField(User, related_name='attending_events')
|
||||
organization_id = models.IntegerField(null=True, blank=True)
|
||||
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)
|
||||
@ -137,9 +234,18 @@ class Report(models.Model):
|
||||
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')
|
||||
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)
|
||||
@ -148,7 +254,53 @@ class Message(models.Model):
|
||||
ordering = ['timestamp']
|
||||
|
||||
def __str__(self):
|
||||
return f"From {self.sender.username} to {self.recipient.username} at {self.timestamp}"
|
||||
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 = [
|
||||
|
||||
BIN
core/static/core/images/logo.png
Normal file
BIN
core/static/core/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
@ -19,21 +19,33 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand brand-font fw-bold fs-4" href="/">CommonGround</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<nav class="navbar navbar-expand-lg sticky-top py-2">
|
||||
<div class="container-fluid px-lg-5">
|
||||
<a class="navbar-brand brand-font fs-4 d-flex align-items-center" href="/">
|
||||
<img src="{% static 'core/images/logo.png' %}" alt="CommonGround" class="me-2" style="height: 24px;">
|
||||
<span class="fw-semibold ls-1" style="font-size: 1.25rem;">CommonGround</span>
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</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' %}">Discover</a>
|
||||
<a class="nav-link px-3" href="{% url 'home' %}">Teammates</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 Teammates</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>
|
||||
@ -72,19 +84,23 @@
|
||||
<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>
|
||||
Discover
|
||||
Teammates
|
||||
</a>
|
||||
<a href="{% url 'home' %}" class="bottom-nav-item">
|
||||
<i class="bi bi-view-stacked"></i>
|
||||
Feed
|
||||
<a href="{% url 'matches' %}" class="bottom-nav-item {% if 'matches' in request.path %}active{% endif %}">
|
||||
<i class="bi bi-people"></i>
|
||||
Teammates
|
||||
</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-dots"></i>
|
||||
Messages
|
||||
</a>
|
||||
<a href="#" class="bottom-nav-item">
|
||||
<a href="{% url 'events' %}" class="bottom-nav-item {% if request.resolver_match.url_name == 'events' %}active{% endif %}">
|
||||
<i class="bi bi-calendar-event"></i>
|
||||
Events
|
||||
Sessions
|
||||
</a>
|
||||
<a href="{% url 'groups:hub' %}" class="bottom-nav-item {% if 'groups' in request.path %}active{% endif %}">
|
||||
<i class="bi bi-grid"></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"></i>
|
||||
|
||||
@ -29,8 +29,44 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="headline" class="form-label fw-semibold">Professional Headline</label>
|
||||
<input type="text" class="form-control" id="headline" name="headline" value="{{ profile.professional_headline }}" placeholder="e.g. Software Engineer | Yoga Enthusiast">
|
||||
<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">
|
||||
@ -39,8 +75,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="bio" class="form-label fw-semibold">Bio</label>
|
||||
<textarea class="form-control" id="bio" name="bio" rows="4" placeholder="Tell others about yourself, your interests, and what you're looking for...">{{ profile.bio }}</textarea>
|
||||
<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">
|
||||
|
||||
29
core/templates/core/event_confirm_delete.html
Normal file
29
core/templates/core/event_confirm_delete.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Delete Event | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="bg-danger-subtle rounded-circle d-inline-flex p-3 mb-3">
|
||||
<i class="bi bi-exclamation-triangle text-danger h3 mb-0"></i>
|
||||
</div>
|
||||
<h2 class="fw-bold h4 mb-3">Delete Event?</h2>
|
||||
<p class="text-muted small mb-4">Are you sure you want to delete "<strong>{{ event.title }}</strong>"? This action cannot be undone and all RSVPs will be lost.</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-danger rounded-pill">Yes, Delete Event</button>
|
||||
<a href="{% url 'event_detail' event.id %}" class="btn btn-light rounded-pill">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
149
core/templates/core/event_detail.html
Normal file
149
core/templates/core/event_detail.html
Normal file
@ -0,0 +1,149 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ event.title }} | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Event Header -->
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden mb-4">
|
||||
{% if event.cover_image %}
|
||||
<img src="{{ event.cover_image.url }}" class="w-100" style="max-height: 400px; object-fit: cover;">
|
||||
{% endif %}
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<span class="badge rounded-pill bg-dark extra-small mb-2">{{ event.get_visibility_display }}</span>
|
||||
<h1 class="h2 fw-bold mb-1">{{ event.title }}</h1>
|
||||
<p class="text-muted"><i class="bi bi-geo-alt me-1"></i> {{ event.location_name }}{% if event.city %}, {{ event.city }}{% endif %}</p>
|
||||
</div>
|
||||
{% if event.creator == user %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-light rounded-circle" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
|
||||
<li><a class="dropdown-item small" href="{% url 'event_edit' event.id %}"><i class="bi bi-pencil me-2"></i> Edit Session</a></li>
|
||||
...
|
||||
<div class="mb-4">
|
||||
<h5 class="fw-bold h6 mb-3">About this session</h5>
|
||||
<div class="small text-secondary" style="white-space: pre-line;">
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if event.is_online and event.online_url %}
|
||||
<div class="alert alert-primary border-0 rounded-4 d-flex align-items-center gap-3">
|
||||
<i class="bi bi-laptop h4 mb-0"></i>
|
||||
<div>
|
||||
<p class="small fw-bold mb-0">This is an online session</p>
|
||||
<a href="{{ event.online_url }}" class="extra-small text-decoration-none" target="_blank">{{ event.online_url }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-sm-6">
|
||||
<div class="d-flex align-items-center gap-3 p-3 bg-light rounded-3">
|
||||
<div class="bg-white rounded-3 p-2 shadow-sm text-center" style="min-width: 60px;">
|
||||
<span class="d-block extra-small text-uppercase fw-bold text-primary">{{ event.start_datetime|date:"M" }}</span>
|
||||
<span class="h4 fw-bold mb-0">{{ event.start_datetime|date:"d" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="small fw-bold mb-0">{{ event.start_datetime|date:"l, P" }}</p>
|
||||
<p class="extra-small text-muted mb-0">{{ event.timezone }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="d-flex align-items-center gap-3 p-3 bg-light rounded-3 h-100">
|
||||
<div class="bg-white rounded-3 p-2 shadow-sm d-flex align-items-center justify-content-center" style="min-width: 60px; height: 60px;">
|
||||
<i class="bi bi-people h4 mb-0 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="small fw-bold mb-0">{{ going_count }} Going</p>
|
||||
<p class="extra-small text-muted mb-0">{% if event.capacity %}{{ event.capacity }} capacity{% else %}Unlimited space{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="fw-bold h6 mb-3">About this event</h5>
|
||||
<div class="small text-secondary" style="white-space: pre-line;">
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if event.is_online and event.online_url %}
|
||||
<div class="alert alert-primary border-0 rounded-4 d-flex align-items-center gap-3">
|
||||
<i class="bi bi-laptop h4 mb-0"></i>
|
||||
<div>
|
||||
<p class="small fw-bold mb-0">This is an online event</p>
|
||||
<a href="{{ event.online_url }}" class="extra-small text-decoration-none" target="_blank">{{ event.online_url }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- RSVP Widget -->
|
||||
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold h6 mb-3">RSVP</h5>
|
||||
{% if user_rsvp %}
|
||||
<div class="alert alert-{% if user_rsvp.status == 'going' %}success{% elif user_rsvp.status == 'maybe' %}info{% else %}secondary{% endif %} border-0 small mb-3">
|
||||
You are marked as: <strong>{{ user_rsvp.get_status_display }}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="{% url 'event_rsvp' event.id %}" method="POST" class="d-grid gap-2">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="status" value="going" class="btn {% if user_rsvp.status == 'going' %}btn-primary-cg{% else %}btn-outline-primary-cg{% endif %} rounded-pill btn-sm">Going</button>
|
||||
<button type="submit" name="status" value="maybe" class="btn {% if user_rsvp.status == 'maybe' %}btn-primary-cg{% else %}btn-outline-primary-cg{% endif %} rounded-pill btn-sm">Maybe</button>
|
||||
<button type="submit" name="status" value="not_going" class="btn {% if user_rsvp.status == 'not_going' %}btn-primary-cg{% else %}btn-outline-primary-cg{% endif %} rounded-pill btn-sm">Not Going</button>
|
||||
{% if user_rsvp %}
|
||||
<button type="submit" name="status" value="cancel" class="btn btn-link text-muted extra-small text-decoration-none">Cancel RSVP</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organizer Widget -->
|
||||
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||
<div class="card-body p-4 text-center">
|
||||
<h5 class="fw-bold h6 mb-3 text-start">Organizer</h5>
|
||||
<img src="{{ event.creator.profile.get_avatar_url }}" class="rounded-circle mb-2" width="64" height="64">
|
||||
<h6 class="fw-bold mb-1">{{ event.creator.get_full_name|default:event.creator.username }}</h6>
|
||||
<p class="extra-small text-muted mb-3">{{ event.creator.profile.professional_headline }}</p>
|
||||
<a href="{% url 'profile_detail' event.creator.username %}" class="btn btn-outline-dark btn-sm rounded-pill w-100 extra-small">View Profile</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attendees Widget -->
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold h6 mb-3">Attendees ({{ going_count }})</h5>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for rsvp in rsvps %}
|
||||
{% if rsvp.status == 'going' %}
|
||||
<a href="{% url 'profile_detail' rsvp.user.username %}" title="{{ rsvp.user.username }}">
|
||||
<img src="{{ rsvp.user.profile.get_avatar_url }}" class="rounded-circle" width="32" height="32" style="object-fit: cover;">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<p class="extra-small text-muted mb-0">No one yet. Be the first!</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
126
core/templates/core/event_form.html
Normal file
126
core/templates/core/event_form.html
Normal file
@ -0,0 +1,126 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }} | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h2 class="fw-bold mb-4">{{ title }}</h2>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold">Session Title*</label>
|
||||
{{ form.title }}
|
||||
{{ form.title.errors }}
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold">Description*</label>
|
||||
{{ form.description }}
|
||||
{{ form.description.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Date & Time -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Start Date & Time*</label>
|
||||
{{ form.start_datetime }}
|
||||
{{ form.start_datetime.errors }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">End Date & Time</label>
|
||||
{{ form.end_datetime }}
|
||||
{{ form.end_datetime.errors }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Timezone</label>
|
||||
{{ form.timezone }}
|
||||
{{ form.timezone.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Location Name*</label>
|
||||
{{ form.location_name }}
|
||||
{{ form.location_name.errors }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold">Address</label>
|
||||
{{ form.location_address }}
|
||||
{{ form.location_address.errors }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">City</label>
|
||||
{{ form.city }}
|
||||
{{ form.city.errors }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">State</label>
|
||||
{{ form.state }}
|
||||
{{ form.state.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Online Info -->
|
||||
<div class="col-12 mt-4">
|
||||
<div class="form-check form-switch">
|
||||
{{ form.is_online }}
|
||||
<label class="form-check-label small fw-bold" for="{{ form.is_online.id_for_label }}">This is an online session</label>
|
||||
</div>
|
||||
</div>
|
||||
...
|
||||
<div class="d-flex justify-content-between align-items-center mt-5">
|
||||
<a href="{% if event %}{% url 'event_detail' event.id %}{% else %}{% url 'events' %}{% endif %}" class="btn btn-link text-muted text-decoration-none">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary-cg rounded-pill px-5">Save Session</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold">Online Meeting URL</label>
|
||||
{{ form.online_url }}
|
||||
{{ form.online_url.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="col-md-6 mt-4">
|
||||
<label class="form-label small fw-bold">Visibility</label>
|
||||
{{ form.visibility }}
|
||||
{{ form.visibility.errors }}
|
||||
</div>
|
||||
<div class="col-md-6 mt-4">
|
||||
<label class="form-label small fw-bold">Capacity</label>
|
||||
{{ form.capacity }}
|
||||
{{ form.capacity.errors }}
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<label class="form-label small fw-bold">Cover Image</label>
|
||||
{{ form.cover_image }}
|
||||
{{ form.cover_image.errors }}
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<label class="form-label small fw-bold">Tags</label>
|
||||
{{ form.tags }}
|
||||
{{ form.tags.errors }}
|
||||
<p class="extra-small text-muted mt-1">Hold Ctrl (or Cmd) to select multiple tags.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-5">
|
||||
<a href="{% if event %}{% url 'event_detail' event.id %}{% else %}{% url 'events' %}{% endif %}" class="btn btn-link text-muted text-decoration-none">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary-cg rounded-pill px-5">Save Event</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
106
core/templates/core/events_list.html
Normal file
106
core/templates/core/events_list.html
Normal file
@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Sessions | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid dashboard-container px-lg-5">
|
||||
<div class="row">
|
||||
<!-- LEFT SIDEBAR -->
|
||||
<div class="col-lg-2 d-none d-lg-block">
|
||||
<div class="sticky-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a href="{% url 'home' %}" class="nav-link"><i class="bi bi-compass"></i> Teammates</a>
|
||||
<a href="{% url 'matches' %}" class="nav-link"><i class="bi bi-people"></i> My Teammates</a>
|
||||
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||
<a href="{% url 'events' %}" class="nav-link active"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||
<a href="{% url 'groups:hub' %}" class="nav-link"><i class="bi bi-grid"></i> Squads</a>
|
||||
<a href="{% url 'my_posts' %}" class="nav-link"><i class="bi bi-journal-text"></i> My Posts</a>
|
||||
<a href="{% url 'my_profile' %}" class="nav-link"><i class="bi bi-person"></i> Profile</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<div class="col-lg-10">
|
||||
<div class="d-flex justify-content-between align-items-end mb-5 pt-2">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">Sessions</h1>
|
||||
<p class="text-muted mb-0">Coordinate and play with your teammates.</p>
|
||||
</div>
|
||||
<a href="{% url 'event_create' %}" class="btn btn-primary-cg">
|
||||
<i class="bi bi-plus-lg me-2"></i> Create Session
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tabs & Search -->
|
||||
<div class="card border-0 shadow-soft rounded-cg mb-5">
|
||||
<div class="card-body p-4">
|
||||
<div class="row align-items-center g-4">
|
||||
<div class="col-md-7">
|
||||
<div class="d-flex gap-3 overflow-auto pb-1">
|
||||
<a href="?tab=upcoming" class="btn btn-sm {% if tab == 'upcoming' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Upcoming</a>
|
||||
<a href="?tab=calendar" class="btn btn-sm {% if tab == 'calendar' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Calendar</a>
|
||||
<a href="?tab=attending" class="btn btn-sm {% if tab == 'attending' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Attending</a>
|
||||
<a href="?tab=mine" class="btn btn-sm {% if tab == 'mine' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">My Sessions</a>
|
||||
<a href="?tab=past" class="btn btn-sm {% if tab == 'past' %}btn-primary-cg{% else %}btn-outline-cg border-0 bg-secondary-section{% endif %} rounded-pill px-4">Past</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<form action="" method="GET" class="input-group">
|
||||
<input type="hidden" name="tab" value="{{ tab }}">
|
||||
<input type="text" name="q" class="form-control border-0 bg-secondary-section rounded-start-cg px-4" placeholder="Search sessions..." value="{{ query }}">
|
||||
<button class="btn btn-outline-cg border-0 bg-secondary-section rounded-end-cg px-4" type="submit"><i class="bi bi-search text-muted"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Grid -->
|
||||
<div class="row g-5">
|
||||
{% if tab == 'calendar' %}
|
||||
{% for date, day_events in calendar_events.items %}
|
||||
<div class="col-12 mb-5">
|
||||
<div class="d-flex align-items-center gap-4 mb-4">
|
||||
<h4 class="mb-0 text-slate opacity-75" style="min-width: 140px;">{{ date|date:"D, M d" }}</h4>
|
||||
<div class="flex-grow-1 border-top" style="border-color: var(--cg-divider) !important;"></div>
|
||||
</div>
|
||||
<div class="row g-5">
|
||||
{% for event in day_events %}
|
||||
<div class="col-md-6 col-xl-4">
|
||||
{% include "core/includes/event_card.html" with event=event %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||
<i class="bi bi-calendar-x display-1 text-muted opacity-25 mb-4 d-block"></i>
|
||||
<h4 class="fw-bold">No sessions scheduled</h4>
|
||||
<p class="text-muted">Stay tuned for upcoming squad sessions.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for event in events %}
|
||||
<div class="col-md-6 col-xl-4">
|
||||
{% include "core/includes/event_card.html" with event=event %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||
<i class="bi bi-calendar-x display-1 text-muted opacity-25 mb-4 d-block"></i>
|
||||
<h4 class="fw-bold">No sessions found</h4>
|
||||
<p class="text-muted">Try adjusting your filters or search query.</p>
|
||||
<a href="{% url 'event_create' %}" class="btn btn-primary-cg mt-4">Create a session!</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
core/templates/core/includes/event_card.html
Normal file
40
core/templates/core/includes/event_card.html
Normal file
@ -0,0 +1,40 @@
|
||||
<div class="card border-0 shadow-soft rounded-cg h-100 overflow-hidden event-card">
|
||||
{% if event.cover_image %}
|
||||
<img src="{{ event.cover_image.url }}" class="card-img-top" style="height: 180px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="bg-secondary-section d-flex align-items-center justify-content-center" style="height: 180px;">
|
||||
<i class="bi bi-calendar-event text-muted opacity-25" style="font-size: 3rem;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body p-4 d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="intent-badge border-0" style="background: var(--cg-sand-primary); font-size: 0.7rem;">
|
||||
{{ event.get_visibility_display }}
|
||||
</span>
|
||||
<p class="small fw-bold text-slate mb-0 opacity-75">
|
||||
{{ event.start_datetime|date:"M d" }}
|
||||
</p>
|
||||
</div>
|
||||
<h5 class="fw-bold h6 mb-2">
|
||||
<a href="{% url 'event_detail' event.id %}" class="text-slate text-decoration-none stretched-link">{{ event.title }}</a>
|
||||
</h5>
|
||||
<p class="text-muted small mb-4 line-clamp-2">{{ event.description }}</p>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="d-flex align-items-center gap-2 mb-4">
|
||||
<i class="bi bi-geo-alt text-muted small"></i>
|
||||
<span class="small text-muted text-truncate">{{ event.location_name }}{% if event.city %}, {{ event.city }}{% endif %}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ event.creator.profile.get_avatar_url }}" class="rounded-circle me-2" width="28" height="28" style="object-fit: cover;">
|
||||
<span class="small text-muted">By {{ event.creator.first_name }}</span>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-people me-1"></i> {{ event.rsvp_count }}{% if event.capacity %}/{{ event.capacity }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2,7 +2,7 @@
|
||||
{% load static %}
|
||||
{% load social_filters %}
|
||||
|
||||
{% block title %}Dashboard | CommonGround{% endblock %}
|
||||
{% block title %}Teammates | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if user.is_authenticated %}
|
||||
@ -12,14 +12,14 @@
|
||||
<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 active"><i class="bi bi-compass"></i> Discover</a>
|
||||
<a href="#" class="nav-link"><i class="bi bi-people"></i> My Matches</a>
|
||||
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||
<a href="#" class="nav-link"><i class="bi bi-calendar-event"></i> Events</a>
|
||||
<a href="#" class="nav-link"><i class="bi bi-grid"></i> Groups</a>
|
||||
<a href="{% url 'my_profile' %}" 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>
|
||||
<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 request.resolver_match.url_name == 'matches' %}active{% endif %}"><i class="bi bi-people"></i> My Teammates</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-dots"></i> Messages</a>
|
||||
<a href="{% url 'events' %}" class="nav-link {% if request.resolver_match.url_name == 'events' %}active{% endif %}"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||
<a href="{% url 'groups:hub' %}" class="nav-link {% if 'groups' in request.path %}active{% endif %}"><i class="bi bi-grid"></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"></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>
|
||||
@ -27,78 +27,81 @@
|
||||
<!-- CENTER COLUMN -->
|
||||
<div class="col-lg-7 col-md-12">
|
||||
<!-- Welcome Header -->
|
||||
<div class="mb-4 pt-2">
|
||||
<h2 class="h3 fw-bold mb-1">Welcome back, {{ user.first_name|default:user.username }}</h2>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge rounded-pill bg-dark extra-small">{{ user.profile.get_transition_status_display }}</span>
|
||||
<span class="text-muted extra-small"><i class="bi bi-geo-alt me-1"></i> {{ user.profile.location_city|default:"Location not set" }}</span>
|
||||
<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="card border-0 shadow-sm mb-4 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<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-3 mb-3">
|
||||
<img src="{{ user.profile.get_avatar_url }}" class="rounded-circle" width="48" height="48" style="object-fit: cover;">
|
||||
<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;">
|
||||
<div class="flex-grow-1">
|
||||
<label class="small fw-bold text-muted mb-1">What are you building this week?</label>
|
||||
<textarea name="content" class="form-control border-0 bg-light rounded-3" placeholder="Share a progress update, ask for help, or offer a skill..." rows="2"></textarea>
|
||||
<label class="small fw-bold text-muted mb-2">Looking for a squad or sharing a highlight?</label>
|
||||
<textarea name="content" class="form-control border-0 bg-light" placeholder="Looking for ranked, casual duo, or sharing a play..." rows="2" style="resize: none;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
<select name="post_type" class="form-select form-select-sm border-0 bg-light rounded-pill px-3" style="width: auto; font-size: 0.75rem;">
|
||||
<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 bg-light 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-light btn-sm rounded-pill text-muted">
|
||||
<i class="bi bi-image"></i>
|
||||
<button type="button" class="btn btn-outline-cg border-0 bg-light 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 rounded-pill px-4 btn-sm">Post</button>
|
||||
<button type="submit" class="btn btn-primary-cg btn-sm px-4">Post</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suggested Members (Horizontal Scroll) -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0 h6">Suggested Members</h5>
|
||||
<a href="{% url 'home' %}" class="extra-small text-decoration-none fw-bold">View All</a>
|
||||
<!-- Suggested Teammates -->
|
||||
<div class="dashboard-section">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="section-header mb-0">Suggested Teammates</h2>
|
||||
<a href="{% url 'home' %}" class="small text-decoration-none fw-bold text-muted">View All</a>
|
||||
</div>
|
||||
<div class="horizontal-scroll">
|
||||
{% for profile in suggested_members %}
|
||||
<div class="suggestion-card shadow-sm">
|
||||
<div class="suggestion-card">
|
||||
<a href="{% url 'profile_detail' profile.user.username %}" class="text-decoration-none text-dark">
|
||||
<img src="{{ profile.get_avatar_url }}" class="rounded-circle mb-2" width="50" height="50" style="object-fit: cover;">
|
||||
<h6 class="mb-0 extra-small fw-bold text-truncate">{{ profile.user.first_name }} {{ profile.user.last_name|slice:":1" }}.</h6>
|
||||
<p class="extra-small text-muted mb-2 text-truncate">{{ profile.professional_headline|default:"Member" }}</p>
|
||||
<img src="{{ profile.get_avatar_url }}" class="rounded-circle mb-3" width="56" height="56" style="object-fit: cover;">
|
||||
<h6 class="mb-1 small fw-bold text-truncate">{{ 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:"Gamer" }}</p>
|
||||
</a>
|
||||
{% if profile.user.id in following_ids %}
|
||||
<a href="{% url 'toggle_follow' profile.user.username %}" class="btn btn-outline-primary btn-sm py-1 w-100 extra-small">Following</a>
|
||||
{% else %}
|
||||
<a href="{% url 'toggle_follow' profile.user.username %}" class="btn btn-primary-cg btn-sm py-1 w-100 extra-small">Follow</a>
|
||||
{% endif %}
|
||||
<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 mt-4">
|
||||
<h5 class="fw-bold mb-3 h6">Activity Feed</h5>
|
||||
<div class="feed dashboard-section">
|
||||
<h2 class="section-header mb-4">Activity Feed</h2>
|
||||
{% for post in posts %}
|
||||
<div class="card border-0 shadow-sm mb-4 rounded-4 overflow-hidden">
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-4">
|
||||
<span class="post-badge badge-{{ post.post_type }}">{{ post.get_post_type_display }}</span>
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<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>
|
||||
@ -106,54 +109,43 @@
|
||||
<p class="text-muted extra-small mb-0">{{ post.timestamp|timesince }} ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-muted p-0" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
<li><a class="dropdown-item text-danger extra-small" href="{% url 'hide_post' post.id %}"><i class="bi bi-eye-slash me-2"></i> Hide post</a></li>
|
||||
{% if post.author == user %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger extra-small" href="{% url 'delete_post' post.id %}"><i class="bi bi-trash me-2"></i> Delete</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<span class="post-badge">{{ post.get_post_type_display }}</span>
|
||||
</div>
|
||||
<p class="mb-3 small">{{ post.content }}</p>
|
||||
<p class="mb-4">{{ post.content }}</p>
|
||||
{% if post.image %}
|
||||
<img src="{{ post.image.url }}" class="img-fluid rounded-3 mb-3 w-100" style="max-height: 350px; object-fit: cover;">
|
||||
<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-3 mt-3">
|
||||
<a href="{% url 'toggle_reaction' post.id %}?type=heart" class="text-decoration-none {% if post|reacted_by:user %}text-danger{% else %}text-muted{% endif %} extra-small fw-bold">
|
||||
<i class="bi bi-heart{% if post|reacted_by:user %}-fill text-danger{% endif %} me-1"></i> {{ post.reactions.count }}
|
||||
<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 extra-small fw-bold" type="button" data-bs-toggle="collapse" data-bs-target="#comments-{{ post.id }}">
|
||||
<i class="bi bi-chat me-1"></i> {{ post.comments.count }}
|
||||
<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-3" id="comments-{{ post.id }}">
|
||||
<div class="bg-light p-3 rounded-3">
|
||||
<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-2 mb-2">
|
||||
<img src="{{ comment.author.profile.get_avatar_url }}" class="rounded-circle" width="28" height="28" style="object-fit: cover;">
|
||||
<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-2 rounded-3 shadow-sm">
|
||||
<p class="extra-small fw-bold mb-0">{{ comment.author.username }}</p>
|
||||
<p class="extra-small mb-0">{{ comment.content }}</p>
|
||||
<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-2">
|
||||
<form action="{% url 'add_comment' post.id %}" method="POST" class="mt-3">
|
||||
{% csrf_token %}
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="content" class="form-control border-0 rounded-start-pill px-3 bg-white" placeholder="Write a comment...">
|
||||
<button class="btn btn-primary-cg rounded-end-pill px-3" type="submit">Send</button>
|
||||
<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>
|
||||
@ -161,8 +153,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-5 bg-white rounded-4 shadow-sm">
|
||||
<p class="text-muted small">No posts yet. Start the conversation!</p>
|
||||
<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>
|
||||
@ -170,62 +162,60 @@
|
||||
|
||||
<!-- RIGHT SIDEBAR (Sticky) -->
|
||||
<div class="col-lg-3 d-none d-lg-block">
|
||||
<div class="sticky-sidebar">
|
||||
<div class="sticky-sidebar pt-2">
|
||||
<!-- Quick Stats -->
|
||||
<div class="stat-widget shadow-sm mb-4">
|
||||
<h6 class="fw-bold mb-3 border-bottom pb-2 h6">Your Momentum</h6>
|
||||
<div class="stat-item">
|
||||
<span class="extra-small text-muted">Unread Messages</span>
|
||||
<span class="badge bg-danger rounded-pill extra-small">{{ stats.unread_messages }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="extra-small text-muted">Accountability Streak</span>
|
||||
<span class="fw-bold text-success extra-small">{{ stats.streak }} 🔥</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="extra-small text-muted">Followers</span>
|
||||
<span class="fw-bold extra-small text-info">{{ stats.followers_count }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="extra-small text-muted">Following</span>
|
||||
<span class="fw-bold extra-small text-primary">{{ stats.following_count }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="extra-small text-muted">Upcoming Events</span>
|
||||
<span class="fw-bold extra-small">{{ stats.upcoming_events_count }}</span>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between extra-small mb-1">
|
||||
<span class="text-muted">Profile Completion</span>
|
||||
<span class="fw-bold">{{ stats.completion_percentage }}%</span>
|
||||
<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="progress" style="height: 6px;">
|
||||
<div class="progress-bar bg-primary" style="width: {{ stats.completion_percentage }}%"></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="stat-widget shadow-sm mb-4">
|
||||
<h6 class="fw-bold mb-3 border-bottom pb-2 h6">Quick Actions</h6>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-cg btn-sm text-start extra-small"><i class="bi bi-calendar-plus me-2"></i> Create Event</button>
|
||||
<button class="btn btn-outline-cg btn-sm text-start extra-small"><i class="bi bi-person-plus me-2"></i> Find Partner</button>
|
||||
<button class="btn btn-outline-cg btn-sm text-start extra-small"><i class="bi bi-plus-circle me-2"></i> Start Group</button>
|
||||
<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="stat-widget shadow-sm">
|
||||
<h6 class="fw-bold mb-3 border-bottom pb-2 h6">Upcoming Events</h6>
|
||||
{% for event in suggested_events %}
|
||||
<div class="mb-3 pb-2 border-bottom">
|
||||
<p class="extra-small fw-bold mb-0 text-truncate">{{ event.title }}</p>
|
||||
<p class="extra-small text-muted mb-0">{{ event.start_time|date:"M d, P" }}</p>
|
||||
<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>
|
||||
{% empty %}
|
||||
<p class="extra-small text-muted text-center py-2">No upcoming events found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -234,59 +224,61 @@
|
||||
|
||||
{% else %}
|
||||
<!-- Anonymous Landing Page -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<section class="hero-section py-5">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3">Intentional connections for life's transitions.</h1>
|
||||
<p class="lead text-muted mb-4">CommonGround helps professionals build high-trust, platonic relationships based on shared values and life goals.</p>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="{% url 'get_started' %}" class="btn btn-primary-cg px-4 py-2">Get Started</a>
|
||||
<a href="{% url 'about' %}" class="btn btn-outline-secondary rounded-3 px-4 py-2">Learn More</a>
|
||||
<div class="col-lg-8 text-center">
|
||||
<h1 class="display-3 mb-4">Find your squad.</h1>
|
||||
<p class="lead text-muted mb-5 px-lg-5">Match by game, rank, role, schedule, and vibe. The intentional network for gamers.</p>
|
||||
<div class="d-flex justify-content-center gap-4">
|
||||
<a href="{% url 'signup' %}" class="btn btn-primary-cg px-5">Get Started</a>
|
||||
<a href="{% url 'about' %}" class="btn btn-outline-cg px-5">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5 bg-white">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4 flex-wrap gap-3">
|
||||
<section class="py-5 bg-secondary-section">
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-5 flex-wrap gap-4">
|
||||
<div>
|
||||
<h2 class="h3 mb-1">Discovery Feed</h2>
|
||||
<p class="text-muted small mb-0">Members aligned with current goals and locations.</p>
|
||||
<h2 class="mb-2">Teammate Discovery</h2>
|
||||
<p class="text-muted mb-0">Players aligned with your games and rank.</p>
|
||||
</div>
|
||||
<div class="d-flex overflow-auto pb-2">
|
||||
<a href="{% url 'home' %}" class="filter-pill {% if not current_intent %}active{% endif %} text-decoration-none small">All Aligned</a>
|
||||
<div class="d-flex overflow-auto pb-2 gap-3">
|
||||
<a href="{% url 'home' %}" class="btn btn-sm {% if not current_intent %}btn-primary-cg{% else %}btn-outline-cg bg-white border-0{% endif %} rounded-pill px-4">All Aligned</a>
|
||||
{% for intent in intents %}
|
||||
<a href="?intent={{ intent.name }}" class="filter-pill {% if current_intent == intent.name %}active{% endif %} text-decoration-none small">{{ intent.name }}</a>
|
||||
<a href="?intent={{ intent.name }}" class="btn btn-sm {% if current_intent == intent.name %}btn-primary-cg{% else %}btn-outline-cg bg-white border-0{% endif %} rounded-pill px-4">{{ intent.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="row g-5">
|
||||
{% for profile in profiles %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="profile-card p-4 h-100 d-flex flex-column shadow-sm border-0">
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<img src="{{ profile.get_avatar_url }}" alt="{{ profile.user.get_full_name }}" class="profile-avatar me-3" style="width: 60px; height: 60px;">
|
||||
<div class="profile-card p-5 h-100 d-flex flex-column border-0 shadow-soft">
|
||||
<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-width: 0;">
|
||||
<div class="pt-1">
|
||||
<span class="transition-tag extra-small">{{ profile.get_transition_status_display }}</span>
|
||||
<h3 class="h6 mb-1 fw-bold">{{ profile.user.first_name }} {{ profile.user.last_name|slice:":1" }}.</h3>
|
||||
<p class="text-muted extra-small mb-0"><i class="bi bi-geo-alt me-1"></i> {{ profile.location_city }}</p>
|
||||
{% 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">{{ 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 small text-cg-slate mb-2">{{ profile.professional_headline }}</p>
|
||||
<p class="text-muted small mb-3 line-clamp-3">{{ profile.bio }}</p>
|
||||
<p class="fw-bold text-slate mb-3">{{ profile.platform|upper }} • {{ profile.rank|default:"Casual Player" }}</p>
|
||||
<p class="text-muted small mb-4 line-clamp-3">{{ profile.bio }}</p>
|
||||
|
||||
<div class="mb-4 d-flex flex-wrap gap-2 mt-auto">
|
||||
<div class="mb-5 d-flex flex-wrap gap-2 mt-auto">
|
||||
{% for intent in profile.intents.all %}
|
||||
<span class="intent-badge extra-small">{{ intent.name }}</span>
|
||||
<span class="intent-badge">{{ intent.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<a href="{% url 'signup' %}" class="btn btn-outline-dark rounded-3 btn-sm py-2">Connect to View</a>
|
||||
<div class="d-grid mt-2">
|
||||
<a href="{% url 'signup' %}" class="btn btn-outline-cg">Connect to View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
134
core/templates/core/matches.html
Normal file
134
core/templates/core/matches.html
Normal file
@ -0,0 +1,134 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load social_filters %}
|
||||
|
||||
{% block title %}Teammates | CommonGround{% 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 %}
|
||||
@ -1,33 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Onboarding - CommonGround{% endblock %}
|
||||
{% block title %}Player Setup - CommonGround{% 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">Welcome to CommonGround</h1>
|
||||
<p class="text-muted">Let's set up your profile to help you find the right connections.</p>
|
||||
<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-sm p-4">
|
||||
<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">Professional Headline</label>
|
||||
<input type="text" name="headline" class="form-control" placeholder="e.g. Architect & Urban Planner" required>
|
||||
<div class="form-text">What do you do? (Keep it professional and clear)</div>
|
||||
<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">Your Bio</label>
|
||||
<textarea name="bio" class="form-control" rows="4" placeholder="Tell us a bit about yourself and why you're here..." required></textarea>
|
||||
<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">Complete Profile</button>
|
||||
<button type="submit" class="btn btn-primary-cg w-100 py-2">Start Matching</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
19
core/templates/core/placeholder.html
Normal file
19
core/templates/core/placeholder.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }} | CommonGround{% 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 %}
|
||||
@ -4,171 +4,158 @@
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0 overflow-hidden">
|
||||
<div class="bg-primary py-5 text-center position-relative">
|
||||
<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"
|
||||
style="width: 150px; height: 150px; object-fit: cover;"
|
||||
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-4 text-center">
|
||||
<h2 class="fw-bold mb-0">{{ target_user.first_name }} {{ target_user.last_name }}</h2>
|
||||
<p class="text-muted mb-3">@{{ target_user.username }}</p>
|
||||
<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-primary mb-3">{{ target_user.profile.professional_headline }}</h5>
|
||||
<h5 class="text-slate mb-4 opacity-75">{{ target_user.profile.professional_headline }}</h5>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-center gap-2 mb-4">
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-geo-alt me-1"></i> {{ target_user.profile.location_city|default:"Location not set" }}
|
||||
<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>
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle">
|
||||
{{ target_user.profile.get_transition_status_display }}
|
||||
{% 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-4 text-secondary">
|
||||
<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-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<a href="{% url 'edit_profile' %}" class="btn btn-primary px-4">
|
||||
<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-secondary px-4">
|
||||
<i class="bi bi-gear me-2"></i>Account Settings
|
||||
<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-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<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-primary px-4">
|
||||
<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 px-4">
|
||||
<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-secondary px-4">
|
||||
<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-4 px-md-5">
|
||||
<div class="row text-center">
|
||||
<div class="col border-end">
|
||||
<h6 class="text-muted text-uppercase small fw-bold">Intents</h6>
|
||||
<div class="mt-2">
|
||||
<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="badge bg-secondary-subtle text-secondary rounded-pill px-3 mb-1 extra-small">
|
||||
<i class="bi {{ intent.icon }} me-1"></i>{{ intent.name }}
|
||||
<span class="intent-badge border-0" style="background: var(--cg-sand-primary);">
|
||||
{{ intent.name }}
|
||||
</span>
|
||||
{% empty %}
|
||||
<small class="text-muted extra-small">No intents shared</small>
|
||||
<small class="text-muted small">No intents shared</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col border-end">
|
||||
<h6 class="text-muted text-uppercase small fw-bold">Values</h6>
|
||||
<div class="mt-2">
|
||||
<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="badge bg-dark-subtle text-dark rounded-pill px-3 mb-1 extra-small">
|
||||
<span class="intent-badge border-0" style="background: var(--cg-sand-secondary);">
|
||||
#{{ tag.name }}
|
||||
</span>
|
||||
{% empty %}
|
||||
<small class="text-muted extra-small">No values shared</small>
|
||||
<small class="text-muted small">No values shared</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h6 class="text-muted text-uppercase small fw-bold">Momentum</h6>
|
||||
<div class="mt-2">
|
||||
<span class="badge bg-success-subtle text-success rounded-pill px-3 mb-1 extra-small">
|
||||
<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>
|
||||
<span class="badge bg-info-subtle text-info rounded-pill px-3 mb-1 extra-small">
|
||||
{{ target_user.profile.followers_count }} Followers
|
||||
</span>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill px-3 mb-1 extra-small">
|
||||
{{ target_user.profile.following_count }} Following
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-md-8">
|
||||
<h3 class="h5 mb-4 px-2 fw-bold">Recent Activity</h3>
|
||||
{% for post in target_user.posts.all %}
|
||||
<div class="card border-0 shadow-sm mb-4 rounded-4 overflow-hidden">
|
||||
<div class="card-body p-4">
|
||||
<span class="post-badge badge-{{ post.post_type }}">{{ post.get_post_type_display }}</span>
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ post.author.profile.get_avatar_url }}" class="rounded-circle me-3" width="48" height="48" 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>
|
||||
{% if post.author == user %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-muted p-0" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
<li><a class="dropdown-item text-danger small" href="{% url 'delete_post' post.id %}"><i class="bi bi-trash me-2"></i> Delete post</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="mb-3">{{ post.content }}</p>
|
||||
{% if post.image %}
|
||||
<img src="{{ post.image.url }}" class="img-fluid rounded-3 mb-3 w-100" style="max-height: 400px; object-fit: cover;">
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-4 border-top pt-3 mt-3">
|
||||
<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 text-danger{% endif %} me-1"></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-1"></i> {{ post.comments.count }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse mt-3" id="comments-{{ post.id }}">
|
||||
<div class="bg-light p-3 rounded-3">
|
||||
{% for comment in post.comments.all %}
|
||||
<div class="d-flex gap-2 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-2 rounded-3 shadow-sm">
|
||||
<p class="small fw-bold mb-1">{{ comment.author.username }}</p>
|
||||
<p class="small mb-0">{{ comment.content }}</p>
|
||||
</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>
|
||||
{% endfor %}
|
||||
<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>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-5 bg-white rounded-3 shadow-sm">
|
||||
<p class="text-muted mb-0">No posts yet.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<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' %}">Sign up</a></p>
|
||||
<p class="small text-muted">Don't have an account? <a href="{% url 'signup' %}">Get Started</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sign Up - CommonGround{% endblock %}
|
||||
{% block title %}Player Setup - CommonGround{% 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">Create your account</h2>
|
||||
<h2 class="brand-font text-center mb-4">Player Setup</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
|
||||
Binary file not shown.
@ -7,3 +7,7 @@ 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)
|
||||
|
||||
26
core/urls.py
26
core/urls.py
@ -4,7 +4,10 @@ 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
|
||||
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 = [
|
||||
@ -21,6 +24,27 @@ urlpatterns = [
|
||||
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"),
|
||||
|
||||
414
core/views.py
414
core/views.py
@ -3,13 +3,59 @@ import platform
|
||||
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from .models import Profile, Intent, ValueTag, Message, Post, Comment, Reaction, HiddenPost, Follow
|
||||
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": "CommonGround",
|
||||
}
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen or member dashboard."""
|
||||
|
||||
@ -62,49 +108,13 @@ def home(request):
|
||||
|
||||
intents = Intent.objects.all()
|
||||
|
||||
# Dashboard logic for logged-in users
|
||||
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': 0, # Placeholder until connection request system exists
|
||||
'upcoming_events_count': request.user.attending_events.filter(start_time__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: Aligned members (shared intents or values)
|
||||
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: Upcoming events
|
||||
from .models import Event
|
||||
suggested_events = Event.objects.filter(start_time__gt=timezone.now()).order_by('start_time')[:5]
|
||||
|
||||
context = {
|
||||
"project_name": "CommonGround",
|
||||
context = get_dashboard_context(request)
|
||||
context.update({
|
||||
"profiles": profiles,
|
||||
"intents": intents,
|
||||
"current_intent": intent_filter,
|
||||
"current_time": timezone.now(),
|
||||
"posts": posts,
|
||||
"stats": stats,
|
||||
"suggested_members": suggested_members,
|
||||
"suggested_events": suggested_events,
|
||||
"post_types": Post.POST_TYPE_CHOICES,
|
||||
"following_ids": following_ids,
|
||||
}
|
||||
})
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
@login_required
|
||||
@ -170,7 +180,7 @@ def signup(request):
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
# Create profile
|
||||
Profile.objects.create(user=user)
|
||||
Profile.objects.get_or_create(user=user)
|
||||
login(request, user)
|
||||
return redirect('onboarding')
|
||||
else:
|
||||
@ -179,15 +189,23 @@ def signup(request):
|
||||
|
||||
@login_required
|
||||
def onboarding(request):
|
||||
# Simplified onboarding for MVP
|
||||
profile = request.user.profile
|
||||
if request.method == 'POST':
|
||||
profile.professional_headline = request.POST.get('headline', '')
|
||||
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')
|
||||
return render(request, 'core/onboarding.html', {'profile': profile})
|
||||
|
||||
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):
|
||||
@ -195,7 +213,6 @@ def settings_view(request):
|
||||
if request.method == 'POST':
|
||||
profile.two_factor_enabled = 'two_factor' in request.POST
|
||||
profile.save()
|
||||
# In a real app we'd save more settings here
|
||||
return redirect('settings')
|
||||
return render(request, 'core/settings.html', {'profile': profile})
|
||||
|
||||
@ -215,7 +232,6 @@ def inbox(request):
|
||||
partner_ids = set(list(sent_to) + list(received_from))
|
||||
partners = User.objects.filter(id__in=partner_ids).select_related('profile')
|
||||
|
||||
# Add last message to each partner for display
|
||||
for partner in partners:
|
||||
last_message = Message.objects.filter(
|
||||
Q(sender=request.user, recipient=partner) |
|
||||
@ -231,32 +247,33 @@ def chat_detail(request, 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)
|
||||
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(
|
||||
Q(sender=request.user, recipient=partner) |
|
||||
Q(sender=partner, recipient=request.user)
|
||||
).order_by('timestamp')
|
||||
|
||||
# Mark as read
|
||||
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
|
||||
'chat_messages': messages,
|
||||
'thread': thread
|
||||
})
|
||||
|
||||
@login_required
|
||||
def profile_view(request):
|
||||
"""Redirect to the current user's profile detail page."""
|
||||
return redirect('profile_detail', username=request.user.username)
|
||||
|
||||
def profile_detail(request, username):
|
||||
"""View a user's profile."""
|
||||
target_user = get_object_or_404(User, username=username)
|
||||
is_following = False
|
||||
if request.user.is_authenticated:
|
||||
@ -268,17 +285,296 @@ def profile_detail(request, username):
|
||||
|
||||
@login_required
|
||||
def edit_profile(request):
|
||||
"""Edit the current user's profile."""
|
||||
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()
|
||||
|
||||
return render(request, 'core/edit_profile.html', {'profile': profile})
|
||||
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
0
groups/__init__.py
Normal file
BIN
groups/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
groups/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
groups/__pycache__/admin.cpython-311.pyc
Normal file
BIN
groups/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
groups/__pycache__/apps.cpython-311.pyc
Normal file
BIN
groups/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
groups/__pycache__/models.cpython-311.pyc
Normal file
BIN
groups/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
groups/__pycache__/urls.cpython-311.pyc
Normal file
BIN
groups/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
groups/__pycache__/views.cpython-311.pyc
Normal file
BIN
groups/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
groups/admin.py
Normal file
3
groups/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
groups/apps.py
Normal file
6
groups/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GroupsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'groups'
|
||||
51
groups/migrations/0001_initial.py
Normal file
51
groups/migrations/0001_initial.py
Normal file
@ -0,0 +1,51 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 18:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Group',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField()),
|
||||
('group_type', models.CharField(choices=[('circle', 'Circle (4–12 members)'), ('community', 'Community (Open)'), ('mastermind', 'Mastermind (Structured Recurring)'), ('event_based', 'Event Based'), ('private_invite', 'Private Invite')], max_length=50)),
|
||||
('intent_type', models.CharField(choices=[('friendship', 'Friendship'), ('networking', 'Networking'), ('activity', 'Activity'), ('accountability', 'Accountability')], max_length=50)),
|
||||
('transition_focus', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('visibility', models.CharField(choices=[('public_members', 'Public (Members Only)'), ('private_request', 'Private (Request to Join)'), ('invite_only', 'Invite Only')], default='public_members', max_length=50)),
|
||||
('capacity', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('location_scope', models.CharField(choices=[('local', 'Local'), ('virtual', 'Virtual')], default='local', max_length=20)),
|
||||
('city', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('state', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('recurring_schedule', models.TextField(blank=True, help_text='Required for Mastermind groups. Use structured text or JSON.', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('cover_image', models.ImageField(blank=True, null=True, upload_to='groups/covers/')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_groups_v2', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupMember',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('member', 'Member'), ('moderator', 'Moderator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('pending', 'Pending')], default='active', max_length=20)),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='groups.group')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_memberships', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('group', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 18:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('groups', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='group',
|
||||
name='group_type',
|
||||
field=models.CharField(choices=[('squad', 'Squad (4–12 members)'), ('community', 'Community (Open)'), ('mastermind', 'Pro Squad (Structured)'), ('tournament', 'Tournament Team'), ('private', 'Private Invite')], max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='group',
|
||||
name='intent_type',
|
||||
field=models.CharField(choices=[('ranked', 'Ranked Squad'), ('duo', 'Duo'), ('casual', 'Casual'), ('tournament', 'Tournament'), ('practice', 'Practice')], max_length=50),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 18:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('groups', '0002_alter_group_group_type_alter_group_intent_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='group',
|
||||
old_name='transition_focus',
|
||||
new_name='focus_game',
|
||||
),
|
||||
]
|
||||
0
groups/migrations/__init__.py
Normal file
0
groups/migrations/__init__.py
Normal file
BIN
groups/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
groups/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
groups/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
groups/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
100
groups/models.py
Normal file
100
groups/models.py
Normal file
@ -0,0 +1,100 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
class Group(models.Model):
|
||||
GROUP_TYPE_CHOICES = [
|
||||
('squad', 'Squad (4–12 members)'),
|
||||
('community', 'Community (Open)'),
|
||||
('mastermind', 'Pro Squad (Structured)'),
|
||||
('tournament', 'Tournament Team'),
|
||||
('private', 'Private Invite'),
|
||||
]
|
||||
|
||||
INTENT_TYPE_CHOICES = [
|
||||
('ranked', 'Ranked Squad'),
|
||||
('duo', 'Duo'),
|
||||
('casual', 'Casual'),
|
||||
('tournament', 'Tournament'),
|
||||
('practice', 'Practice'),
|
||||
]
|
||||
|
||||
VISIBILITY_CHOICES = [
|
||||
('public_members', 'Public (Members Only)'),
|
||||
('private_request', 'Private (Request to Join)'),
|
||||
('invite_only', 'Invite Only'),
|
||||
]
|
||||
|
||||
LOCATION_SCOPE_CHOICES = [
|
||||
('local', 'Local'),
|
||||
('virtual', 'Virtual'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField()
|
||||
group_type = models.CharField(max_length=50, choices=GROUP_TYPE_CHOICES)
|
||||
intent_type = models.CharField(max_length=50, choices=INTENT_TYPE_CHOICES)
|
||||
focus_game = models.CharField(max_length=100, blank=True, null=True)
|
||||
visibility = models.CharField(max_length=50, choices=VISIBILITY_CHOICES, default='public_members')
|
||||
capacity = models.PositiveIntegerField(null=True, blank=True)
|
||||
location_scope = models.CharField(max_length=20, choices=LOCATION_SCOPE_CHOICES, default='local')
|
||||
city = models.CharField(max_length=100, blank=True, null=True)
|
||||
state = models.CharField(max_length=100, blank=True, null=True)
|
||||
recurring_schedule = models.TextField(blank=True, null=True, help_text="Required for Mastermind groups. Use structured text or JSON.")
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_groups_v2')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
cover_image = models.ImageField(upload_to='groups/covers/', blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.group_type == 'circle':
|
||||
if self.capacity and self.capacity > 12:
|
||||
raise ValidationError({'capacity': 'Circles cannot have more than 12 members.'})
|
||||
if not self.capacity:
|
||||
self.capacity = 12
|
||||
|
||||
if self.group_type == 'mastermind' and not self.recurring_schedule:
|
||||
raise ValidationError({'recurring_schedule': 'Mastermind groups require a structured schedule.'})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class GroupMember(models.Model):
|
||||
ROLE_CHOICES = [
|
||||
('member', 'Member'),
|
||||
('moderator', 'Moderator'),
|
||||
('owner', 'Owner'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('active', 'Active'),
|
||||
('pending', 'Pending'),
|
||||
]
|
||||
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name='memberships')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='group_memberships')
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='member')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('group', 'user')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} in {self.group.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.status == 'active':
|
||||
# Use filter().count() instead of exclude() for new objects
|
||||
current_count = GroupMember.objects.filter(group=self.group, status='active').exclude(id=self.id).count()
|
||||
if self.group.capacity and current_count >= self.group.capacity:
|
||||
raise ValidationError('This group has reached its maximum capacity.')
|
||||
155
groups/templates/groups/group_detail.html
Normal file
155
groups/templates/groups/group_detail.html
Normal file
@ -0,0 +1,155 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ group.name }} | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid dashboard-container px-lg-5">
|
||||
<div class="row">
|
||||
<!-- LEFT SIDEBAR -->
|
||||
<div class="col-lg-2 d-none d-lg-block">
|
||||
<div class="sticky-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a href="{% url 'home' %}" class="nav-link"><i class="bi bi-compass"></i> Teammates</a>
|
||||
<a href="{% url 'groups:hub' %}" class="nav-link active"><i class="bi bi-grid"></i> Squads</a>
|
||||
<a href="{% url 'inbox' %}" class="nav-link"><i class="bi bi-chat-dots"></i> Messages</a>
|
||||
<a href="{% url 'events' %}" class="nav-link"><i class="bi bi-calendar-event"></i> Sessions</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN COLUMN -->
|
||||
<div class="col-lg-10 col-md-12">
|
||||
<!-- Group Header Card -->
|
||||
<div class="card border-0 shadow-soft mb-5 overflow-hidden" style="border-radius: 24px;">
|
||||
{% if group.cover_image %}
|
||||
<div style="height: 250px; background: url('{{ group.cover_image.url }}') center/cover no-repeat;"></div>
|
||||
{% else %}
|
||||
<div style="height: 150px; background: linear-gradient(135deg, #F6F3EC 0%, #E5E1D8 100%);"></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-4">
|
||||
<div>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<span class="post-badge">{{ group.get_group_type_display }}</span>
|
||||
<span class="intent-badge">{{ group.get_intent_type_display }}</span>
|
||||
<span class="text-muted small ms-2"><i class="bi bi-geo-alt me-1"></i> {{ group.get_location_scope_display }}</span>
|
||||
</div>
|
||||
<h1 class="display-5 fw-bold mb-2">{{ group.name }}</h1>
|
||||
<p class="text-muted lead mb-0">{{ group.focus_game|default:"Competitive Gaming & Teamwork" }}</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
{% if is_member %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-cg px-4 dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
Member
|
||||
</button>
|
||||
<ul class="dropdown-menu border-0 shadow-sm p-2">
|
||||
<li><a class="dropdown-item rounded small" href="{% url 'groups:members' group.pk %}">View Members</a></li>
|
||||
{% if membership.role == 'owner' or membership.role == 'moderator' %}
|
||||
<li><a class="dropdown-item rounded small" href="{% url 'groups:edit' group.pk %}">Settings</a></li>
|
||||
<li><a class="dropdown-item rounded small" href="{% url 'groups:moderation' group.pk %}">Moderation</a></li>
|
||||
{% endif %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{% url 'groups:leave' group.pk %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item rounded small text-danger">Leave Squad</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% elif is_pending %}
|
||||
<button class="btn btn-outline-cg px-4 disabled">Request Sent</button>
|
||||
{% else %}
|
||||
<form action="{% url 'groups:join' group.pk %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary-cg px-5">Join Squad</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
<!-- Content Column -->
|
||||
<div class="col-lg-8">
|
||||
<div class="dashboard-section">
|
||||
<h2 class="section-header mb-4">About</h2>
|
||||
<div class="bg-white p-4 p-lg-5 rounded-cg shadow-soft">
|
||||
<p class="mb-0" style="white-space: pre-wrap; line-height: 1.8;">{{ group.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if group.group_type == 'mastermind' %}
|
||||
<div class="dashboard-section">
|
||||
<h2 class="section-header mb-4">Practice Schedule</h2>
|
||||
<div class="card border-0 bg-secondary-section p-4" style="border-radius: 16px;">
|
||||
<div class="d-flex gap-4">
|
||||
<div class="stat-icon bg-white text-cg-slate">
|
||||
<i class="bi bi-calendar-check fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 fw-bold">Team Commitment</p>
|
||||
<p class="text-muted small mb-0">{{ group.recurring_schedule }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Feed Placeholder -->
|
||||
<div class="dashboard-section mt-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="section-header mb-0">Squad Feed</h2>
|
||||
<button class="btn btn-outline-cg btn-sm border-0 bg-light px-3">Latest First</button>
|
||||
</div>
|
||||
<div class="text-center py-5 bg-white rounded-cg shadow-soft">
|
||||
<i class="bi bi-chat-dots fs-1 text-muted opacity-25 mb-3 d-block"></i>
|
||||
<p class="text-muted mb-0">The conversation is just beginning. Be the first to coordinate.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="stat-widget mb-5">
|
||||
<h2 class="section-header mb-4" style="font-size: 1.1rem;">Squad Info</h2>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex justify-content-between small border-bottom pb-2">
|
||||
<span class="text-muted">Visibility</span>
|
||||
<span class="fw-bold">{{ group.get_visibility_display }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small border-bottom pb-2">
|
||||
<span class="text-muted">Capacity</span>
|
||||
<span class="fw-bold">{{ active_members.count }} / {{ group.capacity|default:"∞" }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small border-bottom pb-2">
|
||||
<span class="text-muted">Created</span>
|
||||
<span class="fw-bold">{{ group.created_at|date:"M Y" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-widget">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="section-header mb-0" style="font-size: 1.1rem;">Members</h2>
|
||||
<a href="{% url 'groups:members' group.pk %}" class="extra-small text-decoration-none fw-bold">View All</a>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for member in active_members|slice:":12" %}
|
||||
<a href="{% url 'profile_detail' member.user.username %}" title="{{ member.user.username }}">
|
||||
<img src="{{ member.user.profile.get_avatar_url }}" class="rounded-circle border" width="40" height="40" style="object-fit: cover;">
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
117
groups/templates/groups/group_form.html
Normal file
117
groups/templates/groups/group_form.html
Normal file
@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if object %}Edit{% else %}Create{% endif %} Squad | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="dashboard-section text-center mb-5">
|
||||
<h1 class="welcome-header">{% if object %}Refine your Squad{% else %}Start a new Squad{% endif %}</h1>
|
||||
<p class="text-muted">Set the coordination and goals for your team.</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-soft p-4 p-lg-5" style="border-radius: 24px;">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold small">Squad Name</label>
|
||||
<input type="text" name="name" class="form-control px-4 py-3 bg-light border-0" value="{{ form.name.value|default:'' }}" required placeholder="e.g. Apex Predators: Ranked Climb">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold small">Description & Goals</label>
|
||||
<textarea name="description" class="form-control px-4 py-3 bg-light border-0" rows="5" required placeholder="What are the goals of this squad? Who are you looking for?">{{ form.description.value|default:'' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Squad Type</label>
|
||||
<select name="group_type" class="form-select px-4 py-3 bg-light border-0" required id="id_group_type">
|
||||
{% for code, label in form.fields.group_type.choices %}
|
||||
<option value="{{ code }}" {% if form.group_type.value == code %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Intent Type</label>
|
||||
<select name="intent_type" class="form-select px-4 py-3 bg-light border-0" required>
|
||||
{% for code, label in form.fields.intent_type.choices %}
|
||||
<option value="{{ code }}" {% if form.intent_type.value == code %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold small">Focus Game / Title (Optional)</label>
|
||||
<input type="text" name="focus_game" class="form-control px-4 py-3 bg-light border-0" value="{{ form.focus_game.value|default:'' }}" placeholder="e.g. Apex Legends, Valorant, Tournament Prep">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Visibility</label>
|
||||
<select name="visibility" class="form-select px-4 py-3 bg-light border-0" required>
|
||||
{% for code, label in form.fields.visibility.choices %}
|
||||
<option value="{{ code }}" {% if form.visibility.value == code %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Capacity (Optional)</label>
|
||||
<input type="number" name="capacity" class="form-control px-4 py-3 bg-light border-0" value="{{ form.capacity.value|default:'' }}" placeholder="e.g. 5">
|
||||
<small class="text-muted extra-small">Small Squads are capped at 12 by default.</small>
|
||||
</div>
|
||||
|
||||
<div class="col-12" id="mastermind_schedule_div" style="display: none;">
|
||||
<label class="form-label fw-bold small">Practice Schedule (Required for Pro Squads)</label>
|
||||
<textarea name="recurring_schedule" class="form-control px-4 py-3 bg-light border-0" rows="2" placeholder="e.g. Every Mon/Wed/Fri at 8 PM EST">{{ form.recurring_schedule.value|default:'' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Location Scope</label>
|
||||
<select name="location_scope" class="form-select px-4 py-3 bg-light border-0" required>
|
||||
{% for code, label in form.fields.location_scope.choices %}
|
||||
<option value="{{ code }}" {% if form.location_scope.value == code %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Cover Image</label>
|
||||
<input type="file" name="cover_image" class="form-control px-4 py-3 bg-light border-0">
|
||||
</div>
|
||||
|
||||
<div class="col-12 pt-4">
|
||||
<div class="d-flex gap-3 justify-content-center">
|
||||
<a href="{% url 'groups:hub' %}" class="btn btn-outline-cg px-5 py-3">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary-cg px-5 py-3">{% if object %}Save Changes{% else %}Launch Squad{% endif %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const typeSelect = document.getElementById('id_group_type');
|
||||
const scheduleDiv = document.getElementById('mastermind_schedule_div');
|
||||
|
||||
function toggleSchedule() {
|
||||
if (typeSelect.value === 'mastermind') {
|
||||
scheduleDiv.style.display = 'block';
|
||||
} else {
|
||||
scheduleDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
typeSelect.addEventListener('change', toggleSchedule);
|
||||
toggleSchedule(); // Initial state
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
38
groups/templates/groups/group_members.html
Normal file
38
groups/templates/groups/group_members.html
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Members | {{ group.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="dashboard-section mb-5">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'groups:hub' %}" class="text-decoration-none">Groups</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'groups:detail' group.pk %}" class="text-decoration-none">{{ group.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Members</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="welcome-header">Community Members</h1>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{% for membership in group.memberships.all %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-soft p-4" style="border-radius: 16px;">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ membership.user.profile.get_avatar_url }}" class="rounded-circle me-3" width="64" height="64" style="object-fit: cover;">
|
||||
<div>
|
||||
<h3 class="h6 mb-1 fw-bold">{{ membership.user.get_full_name|default:membership.user.username }}</h3>
|
||||
<p class="extra-small text-muted mb-2">{{ membership.user.profile.professional_headline|default:"Member" }}</p>
|
||||
<span class="badge {% if membership.role == 'owner' %}bg- cg-slate{% else %}bg-light text-muted{% endif %} extra-small rounded-pill">
|
||||
{{ membership.get_role_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
176
groups/templates/groups/hub.html
Normal file
176
groups/templates/groups/hub.html
Normal file
@ -0,0 +1,176 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Squads Hub | CommonGround{% 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
3
groups/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
15
groups/urls.py
Normal file
15
groups/urls.py
Normal file
@ -0,0 +1,15 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'groups'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.GroupsHubView.as_view(), name='hub'),
|
||||
path('create/', views.GroupCreateView.as_view(), name='create'),
|
||||
path('<int:pk>/', views.GroupDetailView.as_view(), name='detail'),
|
||||
path('<int:pk>/edit/', views.GroupUpdateView.as_view(), name='edit'),
|
||||
path('<int:pk>/members/', views.GroupMembersView.as_view(), name='members'),
|
||||
path('<int:pk>/join/', views.GroupJoinView.as_view(), name='join'),
|
||||
path('<int:pk>/leave/', views.GroupLeaveView.as_view(), name='leave'),
|
||||
path('<int:pk>/moderation/', views.GroupModerationView.as_view(), name='moderation'),
|
||||
]
|
||||
123
groups/views.py
Normal file
123
groups/views.py
Normal file
@ -0,0 +1,123 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Q
|
||||
from .models import Group, GroupMember
|
||||
|
||||
class GroupsHubView(LoginRequiredMixin, ListView):
|
||||
model = Group
|
||||
template_name = 'groups/hub.html'
|
||||
context_object_name = 'discover_groups'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
context['your_circles'] = Group.objects.filter(
|
||||
memberships__user=user,
|
||||
memberships__status='active'
|
||||
).distinct()
|
||||
|
||||
# Simple recommendation based on intent (can be refined later)
|
||||
user_profile = getattr(user, 'profile', None)
|
||||
if user_profile:
|
||||
user_intents = user_profile.intents.all()
|
||||
context['recommended_groups'] = Group.objects.exclude(
|
||||
memberships__user=user
|
||||
).filter(
|
||||
intent_type__in=[i.name.lower() for i in user_intents]
|
||||
).distinct()[:3]
|
||||
|
||||
return context
|
||||
|
||||
class GroupCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Group
|
||||
template_name = 'groups/group_form.html'
|
||||
fields = [
|
||||
'name', 'description', 'group_type', 'intent_type',
|
||||
'focus_game', 'visibility', 'capacity',
|
||||
'location_scope', 'city', 'state', 'recurring_schedule', 'cover_image'
|
||||
]
|
||||
success_url = reverse_lazy('groups:hub')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user
|
||||
response = super().form_valid(form)
|
||||
# Automatically add creator as owner
|
||||
GroupMember.objects.create(
|
||||
group=self.object,
|
||||
user=self.request.user,
|
||||
role='owner',
|
||||
status='active'
|
||||
)
|
||||
return response
|
||||
|
||||
class GroupDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Group
|
||||
template_name = 'groups/group_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
context['is_member'] = self.object.memberships.filter(user=user, status='active').exists()
|
||||
context['is_pending'] = self.object.memberships.filter(user=user, status='pending').exists()
|
||||
context['membership'] = self.object.memberships.filter(user=user).first()
|
||||
context['active_members'] = self.object.memberships.filter(status='active')
|
||||
return context
|
||||
|
||||
class GroupUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Group
|
||||
template_name = 'groups/group_form.html'
|
||||
fields = [
|
||||
'name', 'description', 'group_type', 'intent_type',
|
||||
'focus_game', 'visibility', 'capacity',
|
||||
'location_scope', 'city', 'state', 'recurring_schedule', 'cover_image'
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return Group.objects.filter(memberships__user=self.request.user, memberships__role__in=['owner', 'moderator'])
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('groups:detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
class GroupMembersView(LoginRequiredMixin, DetailView):
|
||||
model = Group
|
||||
template_name = 'groups/group_members.html'
|
||||
|
||||
class GroupJoinView(LoginRequiredMixin, View):
|
||||
def post(self, request, pk):
|
||||
group = get_object_or_404(Group, pk=pk)
|
||||
|
||||
# Check if already a member
|
||||
if group.memberships.filter(user=request.user).exists():
|
||||
return redirect('groups:detail', pk=pk)
|
||||
|
||||
status = 'active'
|
||||
if group.visibility == 'private_request':
|
||||
status = 'pending'
|
||||
|
||||
try:
|
||||
GroupMember.objects.create(
|
||||
group=group,
|
||||
user=request.user,
|
||||
status=status
|
||||
)
|
||||
except Exception as e:
|
||||
# Handle capacity error from model clean
|
||||
pass
|
||||
|
||||
return redirect('groups:detail', pk=pk)
|
||||
|
||||
class GroupLeaveView(LoginRequiredMixin, View):
|
||||
def post(self, request, pk):
|
||||
membership = get_object_or_404(GroupMember, group_id=pk, user=request.user)
|
||||
if membership.role != 'owner':
|
||||
membership.delete()
|
||||
return redirect('groups:hub')
|
||||
|
||||
class GroupModerationView(LoginRequiredMixin, DetailView):
|
||||
model = Group
|
||||
template_name = 'groups/group_moderation.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return Group.objects.filter(memberships__user=self.request.user, memberships__role__in=['owner', 'moderator'])
|
||||
28
seed_games.py
Normal file
28
seed_games.py
Normal file
@ -0,0 +1,28 @@
|
||||
from core.models import Game
|
||||
from django.utils.text import slugify
|
||||
|
||||
games = [
|
||||
{"name": "League of Legends", "genre": "moba", "team_size": 5, "has_roles": True, "roles_json": ["Top", "Jungle", "Mid", "ADC", "Support"]},
|
||||
{"name": "Valorant", "genre": "fps", "team_size": 5, "has_roles": True, "roles_json": ["Duelist", "Initiator", "Controller", "Sentinel"]},
|
||||
{"name": "Counter-Strike 2", "genre": "fps", "team_size": 5, "has_roles": False},
|
||||
{"name": "Apex Legends", "genre": "battle_royale", "team_size": 3, "has_roles": True, "roles_json": ["Offensive", "Defensive", "Support", "Recon"]},
|
||||
{"name": "Overwatch 2", "genre": "fps", "team_size": 5, "has_roles": True, "roles_json": ["Tank", "Damage", "Support"]},
|
||||
{"name": "Dota 2", "genre": "moba", "team_size": 5, "has_roles": True, "roles_json": ["Carry", "Mid", "Offlane", "Soft Support", "Hard Support"]},
|
||||
{"name": "Fortnite", "genre": "battle_royale", "team_size": 4, "has_roles": False},
|
||||
{"name": "Call of Duty: Warzone", "genre": "battle_royale", "team_size": 4, "has_roles": False},
|
||||
{"name": "Rocket League", "genre": "sports", "team_size": 3, "has_roles": False},
|
||||
{"name": "Minecraft", "genre": "other", "team_size": None, "has_roles": False},
|
||||
]
|
||||
|
||||
for game_data in games:
|
||||
Game.objects.get_or_create(
|
||||
name=game_data["name"],
|
||||
defaults={
|
||||
"slug": slugify(game_data["name"]),
|
||||
"genre": game_data["genre"],
|
||||
"team_size": game_data.get("team_size"),
|
||||
"has_roles": game_data["has_roles"],
|
||||
"roles_json": game_data.get("roles_json"),
|
||||
}
|
||||
)
|
||||
print(f"Successfully seeded {len(games)} games.")
|
||||
@ -1,281 +1,202 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:wght@700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400&display=swap');
|
||||
|
||||
:root {
|
||||
--cg-sand: #F4F1EA;
|
||||
--cg-slate: #2C3E50;
|
||||
--cg-blue: #5D9CEC;
|
||||
--cg-border: #E0D8C3;
|
||||
--cg-sand-primary: #F4F1EA;
|
||||
--cg-sand-secondary: #FAF8F4;
|
||||
--cg-white: #FFFFFF;
|
||||
--cg-slate: #2C3E50;
|
||||
--cg-muted: #6B7280;
|
||||
--cg-blue: #5D9CEC;
|
||||
--cg-sage: #8FAF9F;
|
||||
--cg-divider: rgba(44, 62, 80, 0.08);
|
||||
--cg-success: #789688; /* Muted sage-green for success */
|
||||
--cg-radius-sm: 8px;
|
||||
--cg-radius-md: 12px;
|
||||
--cg-radius-lg: 16px;
|
||||
--cg-transition: all 250ms ease;
|
||||
--cg-shadow-soft: 0 4px 12px rgba(44, 62, 80, 0.04);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--cg-sand);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(180deg, #F6F3EC 0%, #F2EFE7 100%);
|
||||
color: var(--cg-slate);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1, .welcome-header {
|
||||
font-family: 'Playfair Display', serif;
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--cg-slate);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, .brand-font {
|
||||
h2, h3, h4, h5, h6, .section-header, .brand-font {
|
||||
font-family: 'Playfair Display', serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--cg-slate);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6B7280 !important;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.navbar {
|
||||
background-color: var(--cg-white);
|
||||
border-bottom: 1px solid var(--cg-border);
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
padding: 0.75rem 0;
|
||||
transition: var(--cg-transition);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 100px 0 60px;
|
||||
background: linear-gradient(180deg, var(--cg-white) 0%, var(--cg-sand) 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
background: var(--cg-white);
|
||||
border: 1px solid var(--cg-border);
|
||||
border-radius: 12px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.profile-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-bottom: 1rem;
|
||||
border: 3px solid var(--cg-sand);
|
||||
}
|
||||
|
||||
.intent-badge {
|
||||
background-color: rgba(93, 156, 236, 0.1);
|
||||
color: var(--cg-blue);
|
||||
border-radius: 20px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.transition-tag {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #888;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-primary-cg {
|
||||
background-color: var(--cg-slate);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 10px 24px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary-cg:hover {
|
||||
background-color: #1a252f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-outline-cg {
|
||||
border: 1px solid var(--cg-slate);
|
||||
color: var(--cg-slate);
|
||||
border-radius: 8px;
|
||||
padding: 10px 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-outline-cg:hover {
|
||||
background-color: var(--cg-slate);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bg-light-cg {
|
||||
background-color: #fcfbf8;
|
||||
}
|
||||
|
||||
.bg-sand {
|
||||
background-color: var(--cg-sand);
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
border: 1px solid var(--cg-border);
|
||||
background: var(--cg-white);
|
||||
padding: 8px 20px;
|
||||
border-radius: 25px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-pill.active {
|
||||
background: var(--cg-slate);
|
||||
color: white;
|
||||
border-color: var(--cg-slate);
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-top: 1px solid var(--cg-border);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 10px 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.bottom-nav-item {
|
||||
text-align: center;
|
||||
color: var(--cg-slate);
|
||||
text-decoration: none;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.bottom-nav-item i {
|
||||
font-size: 1.25rem;
|
||||
display: block;
|
||||
.navbar-brand img {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-bottom: 80px; /* Space for bottom nav */
|
||||
padding-bottom: 100px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
/* Social Feed Styles */
|
||||
.nav-pills .nav-link.active {
|
||||
background-color: var(--cg-slate);
|
||||
}
|
||||
|
||||
.nav-pills .nav-link {
|
||||
color: var(--cg-slate);
|
||||
}
|
||||
|
||||
.upload-btn-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.upload-btn-wrapper input[type=file] {
|
||||
font-size: 100px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.extra-small {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dashboard Layout */
|
||||
.dashboard-container {
|
||||
padding-top: 1rem;
|
||||
section {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* Sidebar Navigation */
|
||||
.sticky-sidebar {
|
||||
position: sticky;
|
||||
top: 5.5rem;
|
||||
height: calc(100vh - 7rem);
|
||||
overflow-y: auto;
|
||||
background: #F1EEE6;
|
||||
border-right: 1px solid rgba(0,0,0,0.05);
|
||||
padding: 24px 16px;
|
||||
height: calc(100vh - 72px);
|
||||
margin-left: -1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link {
|
||||
padding: 10px 16px;
|
||||
border-radius: 50px;
|
||||
margin-bottom: 4px;
|
||||
gap: 12px;
|
||||
color: var(--cg-slate);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link:hover {
|
||||
background-color: rgba(44, 62, 80, 0.05);
|
||||
background-color: rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link.active {
|
||||
background-color: var(--cg-slate);
|
||||
color: white;
|
||||
background-color: rgba(93,156,236,0.12);
|
||||
color: var(--cg-slate);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Post Type Badges */
|
||||
.post-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.05em;
|
||||
.sidebar-nav .nav-link i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.badge-reflection { background-color: #E1F5FE; color: #01579B; }
|
||||
.badge-looking_for { background-color: #FFF3E0; color: #E65100; }
|
||||
.badge-offering { background-color: #E8F5E9; color: #1B5E20; }
|
||||
.badge-event_invite { background-color: #F3E5F5; color: #4A148C; }
|
||||
.badge-progress_update { background-color: #E0F2F1; color: #004D40; }
|
||||
.badge-skill_share { background-color: #FCE4EC; color: #880E4F; }
|
||||
|
||||
/* Horizontal Scroll */
|
||||
.horizontal-scroll {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.horizontal-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.suggestion-card {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--cg-border);
|
||||
text-align: center;
|
||||
/* Components */
|
||||
.card, .profile-card, .suggestion-card {
|
||||
background: var(--cg-white);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.06);
|
||||
transition: var(--cg-transition);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-widget {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--cg-border);
|
||||
padding: 1.25rem;
|
||||
background: var(--cg-white);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.05);
|
||||
padding: 24px;
|
||||
transition: var(--cg-transition);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
.card:hover, .profile-card:hover, .stat-widget:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 36px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.stat-item:last-child {
|
||||
border-bottom: none;
|
||||
.dashboard-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Badges & Tags */
|
||||
.intent-badge {
|
||||
background-color: var(--cg-sand-secondary);
|
||||
color: var(--cg-blue);
|
||||
border: 1px solid rgba(93, 156, 236, 0.2);
|
||||
border-radius: 30px;
|
||||
padding: 4px 14px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.post-badge {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--cg-sand-secondary);
|
||||
color: var(--cg-muted);
|
||||
border: 1px solid var(--cg-divider);
|
||||
}
|
||||
|
||||
/* Specific Page Refinements */
|
||||
.hero-section {
|
||||
padding: 120px 0 80px;
|
||||
background-color: var(--cg-sand-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid var(--cg-divider);
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.bottom-nav-item {
|
||||
color: var(--cg-muted);
|
||||
}
|
||||
|
||||
.bottom-nav-item.active {
|
||||
color: var(--cg-slate);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-control, .form-select {
|
||||
background-color: var(--cg-white);
|
||||
border: 1px solid var(--cg-divider);
|
||||
border-radius: var(--cg-radius-md);
|
||||
padding: 12px 16px;
|
||||
transition: var(--cg-transition);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--cg-blue);
|
||||
box-shadow: 0 0 0 4px rgba(93, 156, 236, 0.05);
|
||||
}
|
||||
|
||||
/* Divider Customization */
|
||||
hr {
|
||||
border-top: 1px solid var(--cg-divider);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Utils */
|
||||
.rounded-cg { border-radius: var(--cg-radius-lg); }
|
||||
.shadow-soft { box-shadow: var(--cg-shadow-soft); }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user