Autosave: 20260217-172745
This commit is contained in:
parent
37aee45705
commit
a08f77aa00
Binary file not shown.
Binary file not shown.
@ -180,3 +180,7 @@ if EMAIL_USE_SSL:
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Media files (User uploads)
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
@ -27,3 +27,4 @@ urlpatterns = [
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,16 @@
|
||||
from django.contrib import admin
|
||||
from .models import Intent, ValueTag, Profile
|
||||
|
||||
@admin.register(Intent)
|
||||
class IntentAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'icon')
|
||||
|
||||
@admin.register(ValueTag)
|
||||
class ValueTagAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
|
||||
@admin.register(Profile)
|
||||
class ProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'professional_headline', 'transition_status', 'location_city')
|
||||
filter_horizontal = ('intents', 'value_tags')
|
||||
|
||||
# Register your models here.
|
||||
|
||||
46
core/migrations/0001_initial.py
Normal file
46
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 16:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Intent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('icon', models.CharField(blank=True, help_text='Bootstrap icon class name', max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ValueTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('professional_headline', models.CharField(blank=True, max_length=255)),
|
||||
('transition_status', models.CharField(choices=[('none', 'Stable'), ('post-divorce', 'Post-Divorce'), ('relocating', 'Relocating'), ('career-change', 'Career Change'), ('new-in-town', 'New in Town')], default='none', max_length=50)),
|
||||
('bio', models.TextField(blank=True)),
|
||||
('location_city', models.CharField(blank=True, max_length=100)),
|
||||
('avatar_url', models.URLField(blank=True, null=True)),
|
||||
('intents', models.ManyToManyField(blank=True, to='core.intent')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('value_tags', models.ManyToManyField(blank=True, to='core.valuetag')),
|
||||
],
|
||||
),
|
||||
]
|
||||
41
core/migrations/0002_profile_is_email_verified_and_more.py
Normal file
41
core/migrations/0002_profile_is_email_verified_and_more.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 16:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_email_verified',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='onboarding_completed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='organization_id',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='two_factor_enabled',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='user',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
55
core/migrations/0003_group_event_report.py
Normal file
55
core/migrations/0003_group_event_report.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 16:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_profile_is_email_verified_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Group',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField()),
|
||||
('is_private', models.BooleanField(default=False)),
|
||||
('organization_id', models.IntegerField(blank=True, null=True)),
|
||||
('members', models.ManyToManyField(related_name='joined_groups', to=settings.AUTH_USER_MODEL)),
|
||||
('moderators', models.ManyToManyField(related_name='moderated_groups', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('description', models.TextField()),
|
||||
('start_time', models.DateTimeField()),
|
||||
('end_time', models.DateTimeField()),
|
||||
('location', models.CharField(max_length=255)),
|
||||
('organization_id', models.IntegerField(blank=True, null=True)),
|
||||
('attendees', models.ManyToManyField(related_name='attending_events', to=settings.AUTH_USER_MODEL)),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='core.group')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Report',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('resolved', models.BooleanField(default=False)),
|
||||
('organization_id', models.IntegerField(blank=True, null=True)),
|
||||
('reported_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reports_received', to=settings.AUTH_USER_MODEL)),
|
||||
('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports_made', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
30
core/migrations/0004_message.py
Normal file
30
core/migrations/0004_message.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 16:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_group_event_report'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('body', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['timestamp'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_message'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='profile',
|
||||
name='avatar_url',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='avatar',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='avatars/'),
|
||||
),
|
||||
]
|
||||
65
core/migrations/0006_post_comment_hiddenpost_reaction.py
Normal file
65
core/migrations/0006_post_comment_hiddenpost_reaction.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_remove_profile_avatar_url_profile_avatar'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='posts/')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-timestamp'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.post')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['timestamp'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HiddenPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hidden_posts', to=settings.AUTH_USER_MODEL)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.post')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'post')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Reaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('reaction_type', models.CharField(choices=[('heart', 'Heart'), ('like', 'Like'), ('smile', 'Smile')], default='heart', max_length=20)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='core.post')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('post', 'user', 'reaction_type')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:17
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_post_comment_hiddenpost_reaction'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='post_type',
|
||||
field=models.CharField(choices=[('reflection', 'Reflection'), ('looking_for', 'Looking For'), ('offering', 'Offering'), ('event_invite', 'Event Invite'), ('progress_update', 'Progress Update'), ('skill_share', 'Skill Share')], default='reflection', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='accountability_streak',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Connection',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections1', to=settings.AUTH_USER_MODEL)),
|
||||
('user2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections2', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user1', 'user2')},
|
||||
},
|
||||
),
|
||||
]
|
||||
28
core/migrations/0008_follow.py
Normal file
28
core/migrations/0008_follow.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-17 17:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_post_post_type_profile_accountability_streak_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Follow',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('followed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_relations', to=settings.AUTH_USER_MODEL)),
|
||||
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_relations', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('follower', 'followed')},
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0004_message.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_message.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0008_follow.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0008_follow.cpython-311.pyc
Normal file
Binary file not shown.
211
core/models.py
211
core/models.py
@ -1,3 +1,212 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Create your models here.
|
||||
class Intent(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
icon = models.CharField(max_length=50, blank=True, help_text="Bootstrap icon class name")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
class ValueTag(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
class Profile(models.Model):
|
||||
TRANSITION_CHOICES = [
|
||||
('none', 'Stable'),
|
||||
('post-divorce', 'Post-Divorce'),
|
||||
('relocating', 'Relocating'),
|
||||
('career-change', 'Career Change'),
|
||||
('new-in-town', 'New in Town'),
|
||||
]
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
professional_headline = models.CharField(max_length=255, blank=True)
|
||||
transition_status = models.CharField(max_length=50, choices=TRANSITION_CHOICES, default='none')
|
||||
bio = models.TextField(blank=True)
|
||||
location_city = models.CharField(max_length=100, blank=True)
|
||||
intents = models.ManyToManyField(Intent, blank=True)
|
||||
value_tags = models.ManyToManyField(ValueTag, blank=True)
|
||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||
|
||||
# Momentum & Engagement
|
||||
accountability_streak = models.IntegerField(default=0)
|
||||
|
||||
# Auth & Security
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
two_factor_enabled = models.BooleanField(default=False)
|
||||
onboarding_completed = models.BooleanField(default=False)
|
||||
|
||||
# Multitenancy (Future Proofing)
|
||||
organization_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def get_avatar_url(self):
|
||||
if self.avatar and hasattr(self.avatar, 'url'):
|
||||
return self.avatar.url
|
||||
return f"https://i.pravatar.cc/150?u={self.user.username}"
|
||||
|
||||
@property
|
||||
def connection_count(self):
|
||||
return Connection.objects.filter(models.Q(user1=self.user) | models.Q(user2=self.user)).count()
|
||||
|
||||
@property
|
||||
def following_count(self):
|
||||
return Follow.objects.filter(follower=self.user).count()
|
||||
|
||||
@property
|
||||
def followers_count(self):
|
||||
return Follow.objects.filter(followed=self.user).count()
|
||||
|
||||
@property
|
||||
def events_attended_count(self):
|
||||
return self.user.attending_events.count()
|
||||
|
||||
@property
|
||||
def profile_completion_percentage(self):
|
||||
steps = 0
|
||||
total_steps = 5
|
||||
if self.professional_headline: steps += 1
|
||||
if self.bio: steps += 1
|
||||
if self.location_city: steps += 1
|
||||
if self.intents.exists(): steps += 1
|
||||
if self.avatar: steps += 1
|
||||
return int((steps / total_steps) * 100)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s Profile"
|
||||
|
||||
class Connection(models.Model):
|
||||
user1 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='connections1')
|
||||
user2 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='connections2')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user1', 'user2')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user1.username} <-> {self.user2.username}"
|
||||
|
||||
class Follow(models.Model):
|
||||
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name='following_relations')
|
||||
followed = models.ForeignKey(User, on_delete=models.CASCADE, related_name='follower_relations')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('follower', 'followed')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.follower.username} follows {self.followed.username}"
|
||||
|
||||
class Group(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField()
|
||||
is_private = models.BooleanField(default=False)
|
||||
moderators = models.ManyToManyField(User, related_name='moderated_groups')
|
||||
members = models.ManyToManyField(User, related_name='joined_groups')
|
||||
organization_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
class Event(models.Model):
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.title)
|
||||
|
||||
class Report(models.Model):
|
||||
reporter = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_made')
|
||||
reported_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_received', null=True, blank=True)
|
||||
content = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
organization_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Report by {self.reporter.username} at {self.timestamp}"
|
||||
|
||||
class Message(models.Model):
|
||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
|
||||
recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages')
|
||||
body = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
is_read = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['timestamp']
|
||||
|
||||
def __str__(self):
|
||||
return f"From {self.sender.username} to {self.recipient.username} at {self.timestamp}"
|
||||
|
||||
class Post(models.Model):
|
||||
POST_TYPE_CHOICES = [
|
||||
('reflection', 'Reflection'),
|
||||
('looking_for', 'Looking For'),
|
||||
('offering', 'Offering'),
|
||||
('event_invite', 'Event Invite'),
|
||||
('progress_update', 'Progress Update'),
|
||||
('skill_share', 'Skill Share'),
|
||||
]
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
|
||||
content = models.TextField()
|
||||
image = models.ImageField(upload_to='posts/', blank=True, null=True)
|
||||
post_type = models.CharField(max_length=20, choices=POST_TYPE_CHOICES, default='reflection')
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-timestamp']
|
||||
|
||||
def __str__(self):
|
||||
return f"Post by {self.author.username} at {self.timestamp}"
|
||||
|
||||
def user_has_reacted(self, user, reaction_type='heart'):
|
||||
if user.is_authenticated:
|
||||
return self.reactions.filter(user=user, reaction_type=reaction_type).exists()
|
||||
return False
|
||||
|
||||
class Comment(models.Model):
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
content = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['timestamp']
|
||||
|
||||
def __str__(self):
|
||||
return f"Comment by {self.author.username} on {self.post}"
|
||||
|
||||
class Reaction(models.Model):
|
||||
REACTION_CHOICES = [
|
||||
('heart', 'Heart'),
|
||||
('like', 'Like'),
|
||||
('smile', 'Smile'),
|
||||
]
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='reactions')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
reaction_type = models.CharField(max_length=20, choices=REACTION_CHOICES, default='heart')
|
||||
|
||||
class Meta:
|
||||
unique_together = ('post', 'user', 'reaction_type')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} {self.reaction_type}ed {self.post}"
|
||||
|
||||
class HiddenPost(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hidden_posts')
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'post')
|
||||
|
||||
@ -3,23 +3,98 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}CommonGround{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<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">
|
||||
<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>
|
||||
</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" href="{% url 'inbox' %}">Messages</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="{% url 'my_profile' %}">Profile</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="{% url 'settings' %}">Settings</a>
|
||||
</li>
|
||||
{% if user.is_staff %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="/admin/">Admin</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-outline-cg ms-lg-3" href="{% url 'logout' %}">Log Out</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="{% url 'login' %}">Log In</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-primary-cg ms-lg-3" href="{% url 'signup' %}">Join Community</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Mobile Bottom Nav -->
|
||||
<div class="bottom-nav d-lg-none">
|
||||
<a href="{% url 'home' %}" class="bottom-nav-item {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
|
||||
<i class="bi bi-compass"></i>
|
||||
Discover
|
||||
</a>
|
||||
<a href="{% url 'home' %}" class="bottom-nav-item">
|
||||
<i class="bi bi-view-stacked"></i>
|
||||
Feed
|
||||
</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">
|
||||
<i class="bi bi-calendar-event"></i>
|
||||
Events
|
||||
</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>
|
||||
Profile
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
49
core/templates/core/about.html
Normal file
49
core/templates/core/about.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About - CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="py-5 bg-light-cg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center text-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="brand-font display-4 mb-4">Our Mission</h1>
|
||||
<p class="lead text-slate fs-4">CommonGround is a platform for intentional, platonic connections for professionals and individuals navigating life transitions.</p>
|
||||
<p class="text-muted">Not a dating app — a high-trust social connection ecosystem centered on shared values, life goals, activities, and authentic engagement.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm p-4">
|
||||
<h3 class="brand-font h4">High Trust</h3>
|
||||
<p>Verified members and moderated communities ensure a safe and respectful environment.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm p-4">
|
||||
<h3 class="brand-font h4">Value Centered</h3>
|
||||
<p>Match based on shared values like growth, community, and adventure.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm p-4">
|
||||
<h3 class="brand-font h4">Intent Driven</h3>
|
||||
<p>Clearly state what you're looking for: networking, friendship, or activity partners.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5 bg-sand">
|
||||
<div class="container text-center">
|
||||
<h2 class="brand-font mb-4">Ready to find your community?</h2>
|
||||
<a href="{% url 'signup' %}" class="btn btn-primary-cg btn-lg">Join CommonGround</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
54
core/templates/core/chat.html
Normal file
54
core/templates/core/chat.html
Normal file
@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Chat with {{ partner.first_name }} | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-header bg-white p-3 border-bottom d-flex align-items-center">
|
||||
<a href="{% url 'inbox' %}" class="btn btn-link text-dark p-0 me-3"><i class="bi bi-chevron-left h4 mb-0"></i></a>
|
||||
<img src="{{ partner.profile.avatar_url|default:'https://ui-avatars.com/api/?name='|add:partner.first_name }}" class="rounded-circle me-2" width="40" height="40" alt="{{ partner.username }}">
|
||||
<div>
|
||||
<h6 class="mb-0 fw-bold">{{ partner.get_full_name|default:partner.username }}</h6>
|
||||
<small class="text-success">Active now</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4" style="height: 450px; overflow-y: auto;" id="message-container">
|
||||
{% for message in chat_messages %}
|
||||
<div class="d-flex mb-3 {% if message.sender == request.user %}justify-content-end{% endif %}">
|
||||
<div class="p-3 rounded-3 {% if message.sender == request.user %}bg-primary text-white{% else %}bg-light text-dark{% endif %}" style="max-width: 75%;">
|
||||
<p class="mb-1">{{ message.body }}</p>
|
||||
<small class="{% if message.sender == request.user %}text-white-50{% else %}text-muted{% endif %} d-block text-end" style="font-size: 0.7rem;">
|
||||
{{ message.timestamp|date:"H:i" }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-5">
|
||||
<p class="text-muted">No messages yet. Say hello to {{ partner.first_name }}!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-white p-3 border-top">
|
||||
<form method="POST" class="d-flex gap-2">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="body" class="form-control rounded-pill border-light bg-light px-3" placeholder="Type a message..." required autocomplete="off">
|
||||
<button type="submit" class="btn btn-primary-cg rounded-circle p-2 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<i class="bi bi-send-fill text-white"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Scroll to bottom of message container
|
||||
const container = document.getElementById('message-container');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
</script>
|
||||
{% endblock %}
|
||||
68
core/templates/core/edit_profile.html
Normal file
68
core/templates/core/edit_profile.html
Normal file
@ -0,0 +1,68 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h2 class="fw-bold mb-4">Edit Profile</h2>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<div class="position-relative d-inline-block">
|
||||
<img src="{{ profile.get_avatar_url }}"
|
||||
class="rounded-circle border border-3 border-light shadow-sm"
|
||||
style="width: 120px; height: 120px; object-fit: cover;"
|
||||
alt="Current Avatar"
|
||||
id="avatar-preview">
|
||||
<label for="avatar-input" class="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle shadow-sm" style="width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="bi bi-camera"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="form-label small text-muted">Click the camera icon to upload a new picture</label>
|
||||
<input type="file" name="avatar" id="avatar-input" class="d-none" accept="image/*" onchange="previewImage(this)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="headline" class="form-label fw-semibold">Professional Headline</label>
|
||||
<input type="text" class="form-control" id="headline" name="headline" value="{{ profile.professional_headline }}" placeholder="e.g. Software Engineer | Yoga Enthusiast">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="location" class="form-label fw-semibold">Location (City)</label>
|
||||
<input type="text" class="form-control" id="location" name="location" value="{{ profile.location_city }}" placeholder="e.g. Austin, TX">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="bio" class="form-label fw-semibold">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>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary py-2 fw-bold text-uppercase">Save Changes</button>
|
||||
<a href="{% url 'my_profile' %}" class="btn btn-outline-secondary py-2 fw-bold text-uppercase">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function previewImage(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('avatar-preview').src = e.target.result;
|
||||
}
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
40
core/templates/core/inbox.html
Normal file
40
core/templates/core/inbox.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Messages | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<h2 class="h3 mb-4">Conversations</h2>
|
||||
|
||||
<div class="list-group shadow-sm rounded-3 overflow-hidden">
|
||||
{% for partner in partners %}
|
||||
<a href="{% url 'chat_detail' partner.username %}" class="list-group-item list-group-item-action p-3 border-0 border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ partner.profile.avatar_url|default:'https://ui-avatars.com/api/?name='|add:partner.first_name }}" class="rounded-circle me-3" width="50" height="50" alt="{{ partner.username }}">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0 fw-bold">{{ partner.get_full_name|default:partner.username }}</h6>
|
||||
<small class="text-muted">{{ partner.last_message.timestamp|date:"M d" }}</small>
|
||||
</div>
|
||||
<p class="text-muted mb-0 small text-truncate" style="max-width: 300px;">
|
||||
{% if partner.last_message.sender == request.user %}You: {% endif %}
|
||||
{{ partner.last_message.body }}
|
||||
</p>
|
||||
</div>
|
||||
{% if not partner.last_message.is_read and partner.last_message.recipient == request.user %}
|
||||
<span class="badge bg-primary rounded-pill">New</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-center py-5">
|
||||
<p class="text-muted mb-0">No conversations yet. Connect with someone from the Discovery Hub!</p>
|
||||
<a href="{% url 'home' %}" class="btn btn-primary-cg btn-sm mt-3">Browse Profiles</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,145 +1,302 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load social_filters %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Dashboard | CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="container-fluid dashboard-container px-lg-5">
|
||||
<div class="row">
|
||||
<!-- LEFT SIDEBAR (Sticky) -->
|
||||
<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>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<!-- Post Composer -->
|
||||
<div class="card border-0 shadow-sm mb-4 rounded-4">
|
||||
<div class="card-body 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="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>
|
||||
</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;">
|
||||
{% 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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="horizontal-scroll">
|
||||
{% for profile in suggested_members %}
|
||||
<div class="suggestion-card shadow-sm">
|
||||
<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>
|
||||
</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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Feed -->
|
||||
<div class="feed mt-4">
|
||||
<h5 class="fw-bold mb-3 h6">Activity Feed</h5>
|
||||
{% for post in posts %}
|
||||
<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="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>
|
||||
<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>
|
||||
</div>
|
||||
<p class="mb-3 small">{{ 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;">
|
||||
{% 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 }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<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-2">
|
||||
<img src="{{ comment.author.profile.get_avatar_url }}" class="rounded-circle" width="28" height="28" 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>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<form action="{% url 'add_comment' post.id %}" method="POST" class="mt-2">
|
||||
{% 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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDEBAR (Sticky) -->
|
||||
<div class="col-lg-3 d-none d-lg-block">
|
||||
<div class="sticky-sidebar">
|
||||
<!-- 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>
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar bg-primary" style="width: {{ stats.completion_percentage }}%"></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>
|
||||
</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>
|
||||
{% empty %}
|
||||
<p class="extra-small text-muted text-center py-2">No upcoming events found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Anonymous Landing Page -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<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>
|
||||
</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">
|
||||
<div>
|
||||
<h2 class="h3 mb-1">Discovery Feed</h2>
|
||||
<p class="text-muted small mb-0">Members aligned with current goals and locations.</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>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{% 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="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>
|
||||
</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>
|
||||
|
||||
<div class="mb-4 d-flex flex-wrap gap-2 mt-auto">
|
||||
{% for intent in profile.intents.all %}
|
||||
<span class="intent-badge extra-small">{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted">No profiles found yet.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
36
core/templates/core/onboarding.html
Normal file
36
core/templates/core/onboarding.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Onboarding - 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>
|
||||
</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">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary-cg w-100 py-2">Complete Profile</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
175
core/templates/core/profile_detail.html
Normal file
175
core/templates/core/profile_detail.html
Normal file
@ -0,0 +1,175 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load social_filters %}
|
||||
|
||||
{% 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="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;"
|
||||
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>
|
||||
|
||||
{% if target_user.profile.professional_headline %}
|
||||
<h5 class="text-primary mb-3">{{ 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" }}
|
||||
</span>
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle">
|
||||
{{ target_user.profile.get_transition_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="px-md-5 mb-4 text-secondary">
|
||||
{{ 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">
|
||||
<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>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
{% if is_following %}
|
||||
<a href="{% url 'toggle_follow' target_user.username %}" class="btn btn-outline-primary px-4">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
{% 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>
|
||||
{% empty %}
|
||||
<small class="text-muted extra-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">
|
||||
{% 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">
|
||||
#{{ tag.name }}
|
||||
</span>
|
||||
{% empty %}
|
||||
<small class="text-muted extra-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">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
core/templates/core/settings.html
Normal file
49
core/templates/core/settings.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Settings - CommonGround{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-4">
|
||||
<div class="card border-0 shadow-sm p-4">
|
||||
<h3 class="brand-font h4 mb-4">Account Settings</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link px-0 text-slate fw-bold" href="#">Preferences</a>
|
||||
<a class="nav-link px-0 text-muted" href="#">Privacy & Visibility</a>
|
||||
<a class="nav-link px-0 text-muted" href="#">Notifications</a>
|
||||
<hr>
|
||||
<a class="nav-link px-0 text-danger" href="{% url 'logout' %}">Log Out</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm p-4">
|
||||
<h4 class="brand-font mb-4">Preferences</h4>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-block fw-bold">Two-Factor Authentication</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="two_factor" id="twoFactorSwitch" {% if profile.two_factor_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="twoFactorSwitch">Enable 2FA for extra security</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-block fw-bold">Email Verification</label>
|
||||
{% if profile.is_email_verified %}
|
||||
<span class="badge bg-success">Verified</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">Pending</span>
|
||||
<p class="small text-muted mt-2">Verification is simulated for MVP.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary-cg px-4">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
core/templates/registration/login.html
Normal file
30
core/templates/registration/login.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Log In - 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">Welcome back</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary-cg w-100">Log In</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<p class="small text-muted">Don't have an account? <a href="{% url 'signup' %}">Sign up</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
core/templates/registration/signup.html
Normal file
56
core/templates/registration/signup.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sign Up - 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>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary-cg w-100">Sign Up</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<p class="small text-muted">Already have an account? <a href="{% url 'login' %}">Log in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: 0.375rem;
|
||||
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
0
core/templatetags/__init__.py
Normal file
0
core/templatetags/__init__.py
Normal file
BIN
core/templatetags/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/templatetags/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/templatetags/__pycache__/social_filters.cpython-311.pyc
Normal file
BIN
core/templatetags/__pycache__/social_filters.cpython-311.pyc
Normal file
Binary file not shown.
9
core/templatetags/social_filters.py
Normal file
9
core/templatetags/social_filters.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='reacted_by')
|
||||
def reacted_by(post, user):
|
||||
if user.is_authenticated:
|
||||
return post.reactions.filter(user=user).exists()
|
||||
return False
|
||||
32
core/urls.py
32
core/urls.py
@ -1,7 +1,33 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from django.urls import path, include
|
||||
from django.contrib.auth import views as auth_views
|
||||
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
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path("about/", about, name="about"),
|
||||
path("signup/", signup, name="signup"),
|
||||
path("onboarding/", onboarding, name="onboarding"),
|
||||
path("settings/", settings_view, name="settings"),
|
||||
path("get-started/", get_started, name="get_started"),
|
||||
path("messages/", inbox, name="inbox"),
|
||||
path("messages/<str:username>/", chat_detail, name="chat_detail"),
|
||||
path("profile/", profile_view, name="my_profile"),
|
||||
path("profile/edit/", edit_profile, name="edit_profile"),
|
||||
path("profile/<str:username>/", profile_detail, name="profile_detail"),
|
||||
path("profile/<str:username>/follow/", toggle_follow, name="toggle_follow"),
|
||||
|
||||
# Social URLs
|
||||
path("post/create/", create_post, name="create_post"),
|
||||
path("post/<int:post_id>/delete/", delete_post, name="delete_post"),
|
||||
path("post/<int:post_id>/comment/", add_comment, name="add_comment"),
|
||||
path("post/<int:post_id>/react/", toggle_reaction, name="toggle_reaction"),
|
||||
path("post/<int:post_id>/hide/", hide_post, name="hide_post"),
|
||||
|
||||
# Auth URLs
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
]
|
||||
|
||||
289
core/views.py
289
core/views.py
@ -1,25 +1,284 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
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 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 home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
"""Render the landing screen or member dashboard."""
|
||||
|
||||
# Simple logic to seed data for the first run
|
||||
if not Intent.objects.exists():
|
||||
intents_data = [
|
||||
('Friendship', 'bi-people'),
|
||||
('Networking', 'bi-briefcase'),
|
||||
('Activity Partner', 'bi-bicycle'),
|
||||
('Accountability', 'bi-check-circle')
|
||||
]
|
||||
for name, icon in intents_data:
|
||||
Intent.objects.create(name=name, icon=icon)
|
||||
|
||||
if not Profile.objects.exists():
|
||||
# Create a demo user/profile
|
||||
demo_user, _ = User.objects.get_or_create(username='marcus_v', first_name='Marcus', last_name='V.')
|
||||
p = Profile.objects.create(
|
||||
user=demo_user,
|
||||
professional_headline='Architect & Urban Planner',
|
||||
transition_status='new-in-town',
|
||||
bio='Passionate about sustainable cities. Recently moved here from Chicago and looking for local communities.',
|
||||
location_city='Austin, TX',
|
||||
)
|
||||
p.intents.add(Intent.objects.get(name='Networking'))
|
||||
|
||||
demo_user2, _ = User.objects.get_or_create(username='sarah_l', first_name='Sarah', last_name='L.')
|
||||
p2 = Profile.objects.create(
|
||||
user=demo_user2,
|
||||
professional_headline='UX Researcher | Growth Mindset',
|
||||
transition_status='post-divorce',
|
||||
bio='Rediscovering my love for hiking and photography. Seeking authentic connections and shared growth.',
|
||||
location_city='Austin, TX',
|
||||
)
|
||||
p2.intents.add(Intent.objects.get(name='Friendship'))
|
||||
|
||||
# Social Feed Logic
|
||||
hidden_post_ids = []
|
||||
if request.user.is_authenticated:
|
||||
hidden_post_ids = HiddenPost.objects.filter(user=request.user).values_list('post_id', flat=True)
|
||||
|
||||
posts = Post.objects.exclude(id__in=hidden_post_ids).select_related('author', 'author__profile').prefetch_related('comments', 'comments__author', 'reactions')
|
||||
|
||||
# Filtering by intent (for discovery)
|
||||
intent_filter = request.GET.get('intent')
|
||||
if intent_filter:
|
||||
profiles = Profile.objects.filter(intents__name__iexact=intent_filter)
|
||||
else:
|
||||
profiles = Profile.objects.all()
|
||||
|
||||
intents = Intent.objects.all()
|
||||
|
||||
# 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": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
"project_name": "CommonGround",
|
||||
"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
|
||||
def create_post(request):
|
||||
if request.method == 'POST':
|
||||
content = request.POST.get('content')
|
||||
image = request.FILES.get('image')
|
||||
post_type = request.POST.get('post_type', 'reflection')
|
||||
if content or image:
|
||||
Post.objects.create(author=request.user, content=content, image=image, post_type=post_type)
|
||||
return redirect('home')
|
||||
|
||||
@login_required
|
||||
def delete_post(request, post_id):
|
||||
post = get_object_or_404(Post, id=post_id, author=request.user)
|
||||
post.delete()
|
||||
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||
|
||||
@login_required
|
||||
def add_comment(request, post_id):
|
||||
if request.method == 'POST':
|
||||
post = get_object_or_404(Post, id=post_id)
|
||||
content = request.POST.get('content')
|
||||
if content:
|
||||
Comment.objects.create(post=post, author=request.user, content=content)
|
||||
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||
|
||||
@login_required
|
||||
def toggle_reaction(request, post_id):
|
||||
post = get_object_or_404(Post, id=post_id)
|
||||
reaction_type = request.GET.get('type', 'heart')
|
||||
reaction, created = Reaction.objects.get_or_create(
|
||||
post=post, user=request.user, reaction_type=reaction_type
|
||||
)
|
||||
if not created:
|
||||
reaction.delete()
|
||||
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||
|
||||
@login_required
|
||||
def toggle_follow(request, username):
|
||||
target_user = get_object_or_404(User, username=username)
|
||||
if target_user == request.user:
|
||||
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||
|
||||
follow, created = Follow.objects.get_or_create(follower=request.user, followed=target_user)
|
||||
if not created:
|
||||
follow.delete()
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER', 'home'))
|
||||
|
||||
@login_required
|
||||
def hide_post(request, post_id):
|
||||
post = get_object_or_404(Post, id=post_id)
|
||||
HiddenPost.objects.get_or_create(user=request.user, post=post)
|
||||
return redirect('home')
|
||||
|
||||
def about(request):
|
||||
return render(request, "core/about.html")
|
||||
|
||||
def signup(request):
|
||||
if request.method == 'POST':
|
||||
form = UserCreationForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
# Create profile
|
||||
Profile.objects.create(user=user)
|
||||
login(request, user)
|
||||
return redirect('onboarding')
|
||||
else:
|
||||
form = UserCreationForm()
|
||||
return render(request, 'registration/signup.html', {'form': form})
|
||||
|
||||
@login_required
|
||||
def onboarding(request):
|
||||
# Simplified onboarding for MVP
|
||||
profile = request.user.profile
|
||||
if request.method == 'POST':
|
||||
profile.professional_headline = request.POST.get('headline', '')
|
||||
profile.bio = request.POST.get('bio', '')
|
||||
profile.onboarding_completed = True
|
||||
profile.save()
|
||||
return redirect('home')
|
||||
return render(request, 'core/onboarding.html', {'profile': profile})
|
||||
|
||||
@login_required
|
||||
def settings_view(request):
|
||||
profile = request.user.profile
|
||||
if request.method == 'POST':
|
||||
profile.two_factor_enabled = 'two_factor' in request.POST
|
||||
profile.save()
|
||||
# In a real app we'd save more settings here
|
||||
return redirect('settings')
|
||||
return render(request, 'core/settings.html', {'profile': profile})
|
||||
|
||||
def get_started(request):
|
||||
if not request.user.is_authenticated:
|
||||
return redirect('signup')
|
||||
if not request.user.profile.onboarding_completed:
|
||||
return redirect('onboarding')
|
||||
return redirect('home')
|
||||
|
||||
@login_required
|
||||
def inbox(request):
|
||||
# Get all users the current user has messaged or received messages from
|
||||
sent_to = Message.objects.filter(sender=request.user).values_list('recipient', flat=True)
|
||||
received_from = Message.objects.filter(recipient=request.user).values_list('sender', flat=True)
|
||||
|
||||
partner_ids = set(list(sent_to) + list(received_from))
|
||||
partners = User.objects.filter(id__in=partner_ids).select_related('profile')
|
||||
|
||||
# Add last message to each partner for display
|
||||
for partner in partners:
|
||||
last_message = Message.objects.filter(
|
||||
Q(sender=request.user, recipient=partner) |
|
||||
Q(sender=partner, recipient=request.user)
|
||||
).order_by('-timestamp').first()
|
||||
partner.last_message = last_message
|
||||
|
||||
return render(request, 'core/inbox.html', {'partners': partners})
|
||||
|
||||
@login_required
|
||||
def chat_detail(request, username):
|
||||
partner = get_object_or_404(User, username=username)
|
||||
if partner == request.user:
|
||||
return redirect('inbox')
|
||||
|
||||
if request.method == 'POST':
|
||||
body = request.POST.get('body')
|
||||
if body:
|
||||
Message.objects.create(sender=request.user, recipient=partner, body=body)
|
||||
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.filter(recipient=request.user, is_read=False).update(is_read=True)
|
||||
|
||||
return render(request, 'core/chat.html', {
|
||||
'partner': partner,
|
||||
'chat_messages': messages
|
||||
})
|
||||
|
||||
@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:
|
||||
is_following = Follow.objects.filter(follower=request.user, followed=target_user).exists()
|
||||
return render(request, 'core/profile_detail.html', {
|
||||
'target_user': target_user,
|
||||
'is_following': is_following
|
||||
})
|
||||
|
||||
@login_required
|
||||
def edit_profile(request):
|
||||
"""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', '')
|
||||
|
||||
if 'avatar' in request.FILES:
|
||||
profile.avatar = request.FILES['avatar']
|
||||
|
||||
profile.save()
|
||||
return redirect('my_profile')
|
||||
|
||||
return render(request, 'core/edit_profile.html', {'profile': profile})
|
||||
|
||||
@ -1,4 +1,281 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:wght@700&display=swap');
|
||||
|
||||
:root {
|
||||
--cg-sand: #F4F1EA;
|
||||
--cg-slate: #2C3E50;
|
||||
--cg-blue: #5D9CEC;
|
||||
--cg-border: #E0D8C3;
|
||||
--cg-white: #FFFFFF;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--cg-sand);
|
||||
color: var(--cg-slate);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, .brand-font {
|
||||
font-family: 'Playfair Display', serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--cg-white);
|
||||
border-bottom: 1px solid var(--cg-border);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-bottom: 80px; /* Space for bottom nav */
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.sticky-sidebar {
|
||||
position: sticky;
|
||||
top: 5.5rem;
|
||||
height: calc(100vh - 7rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link {
|
||||
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;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link:hover {
|
||||
background-color: rgba(44, 62, 80, 0.05);
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link.active {
|
||||
background-color: var(--cg-slate);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.stat-widget {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--cg-border);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stat-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user