diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..eaf2d70 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..e14328a 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..43d0c77 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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' diff --git a/config/urls.py b/config/urls.py index bcfc074..1a48858 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..62a7133 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..efbc4ce 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..e5c2e74 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..15ad71e 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..d64dec8 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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. diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..d4f8512 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/core/migrations/0002_profile_is_email_verified_and_more.py b/core/migrations/0002_profile_is_email_verified_and_more.py new file mode 100644 index 0000000..f9eff0a --- /dev/null +++ b/core/migrations/0002_profile_is_email_verified_and_more.py @@ -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), + ), + ] diff --git a/core/migrations/0003_group_event_report.py b/core/migrations/0003_group_event_report.py new file mode 100644 index 0000000..0f34302 --- /dev/null +++ b/core/migrations/0003_group_event_report.py @@ -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)), + ], + ), + ] diff --git a/core/migrations/0004_message.py b/core/migrations/0004_message.py new file mode 100644 index 0000000..854d9c8 --- /dev/null +++ b/core/migrations/0004_message.py @@ -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'], + }, + ), + ] diff --git a/core/migrations/0005_remove_profile_avatar_url_profile_avatar.py b/core/migrations/0005_remove_profile_avatar_url_profile_avatar.py new file mode 100644 index 0000000..32bf541 --- /dev/null +++ b/core/migrations/0005_remove_profile_avatar_url_profile_avatar.py @@ -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/'), + ), + ] diff --git a/core/migrations/0006_post_comment_hiddenpost_reaction.py b/core/migrations/0006_post_comment_hiddenpost_reaction.py new file mode 100644 index 0000000..c3faf35 --- /dev/null +++ b/core/migrations/0006_post_comment_hiddenpost_reaction.py @@ -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')}, + }, + ), + ] diff --git a/core/migrations/0007_post_post_type_profile_accountability_streak_and_more.py b/core/migrations/0007_post_post_type_profile_accountability_streak_and_more.py new file mode 100644 index 0000000..f1c1b20 --- /dev/null +++ b/core/migrations/0007_post_post_type_profile_accountability_streak_and_more.py @@ -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')}, + }, + ), + ] diff --git a/core/migrations/0008_follow.py b/core/migrations/0008_follow.py new file mode 100644 index 0000000..126f76c --- /dev/null +++ b/core/migrations/0008_follow.py @@ -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')}, + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..30b7ef4 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_profile_is_email_verified_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_profile_is_email_verified_and_more.cpython-311.pyc new file mode 100644 index 0000000..e4ad9a2 Binary files /dev/null and b/core/migrations/__pycache__/0002_profile_is_email_verified_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_group_event_report.cpython-311.pyc b/core/migrations/__pycache__/0003_group_event_report.cpython-311.pyc new file mode 100644 index 0000000..f9a82dd Binary files /dev/null and b/core/migrations/__pycache__/0003_group_event_report.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_message.cpython-311.pyc b/core/migrations/__pycache__/0004_message.cpython-311.pyc new file mode 100644 index 0000000..e2a6337 Binary files /dev/null and b/core/migrations/__pycache__/0004_message.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_remove_profile_avatar_url_profile_avatar.cpython-311.pyc b/core/migrations/__pycache__/0005_remove_profile_avatar_url_profile_avatar.cpython-311.pyc new file mode 100644 index 0000000..2c59b61 Binary files /dev/null and b/core/migrations/__pycache__/0005_remove_profile_avatar_url_profile_avatar.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0006_post_comment_hiddenpost_reaction.cpython-311.pyc b/core/migrations/__pycache__/0006_post_comment_hiddenpost_reaction.cpython-311.pyc new file mode 100644 index 0000000..afa4e1d Binary files /dev/null and b/core/migrations/__pycache__/0006_post_comment_hiddenpost_reaction.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0007_post_post_type_profile_accountability_streak_and_more.cpython-311.pyc b/core/migrations/__pycache__/0007_post_post_type_profile_accountability_streak_and_more.cpython-311.pyc new file mode 100644 index 0000000..60740c2 Binary files /dev/null and b/core/migrations/__pycache__/0007_post_post_type_profile_accountability_streak_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0008_follow.cpython-311.pyc b/core/migrations/__pycache__/0008_follow.cpython-311.pyc new file mode 100644 index 0000000..6456005 Binary files /dev/null and b/core/migrations/__pycache__/0008_follow.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..a5db98c 100644 --- a/core/models.py +++ b/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') diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..0856475 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -3,23 +3,98 @@ - {% block title %}Knowledge Base{% endblock %} + + {% block title %}CommonGround{% endblock %} {% if project_description %} - - - {% endif %} - {% if project_image_url %} - - {% endif %} {% load static %} - + + + + + + {% block head %}{% endblock %} - {% block content %}{% endblock %} + + +
+ {% block content %}{% endblock %} +
+ + +
+ + + Discover + + + + Feed + + + + Messages + + + + Events + + + + Profile + +
+ + + + {% block scripts %}{% endblock %} diff --git a/core/templates/core/about.html b/core/templates/core/about.html new file mode 100644 index 0000000..522207c --- /dev/null +++ b/core/templates/core/about.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block title %}About - CommonGround{% endblock %} + +{% block content %} +
+
+
+
+

Our Mission

+

CommonGround is a platform for intentional, platonic connections for professionals and individuals navigating life transitions.

+

Not a dating app — a high-trust social connection ecosystem centered on shared values, life goals, activities, and authentic engagement.

+
+
+
+
+ +
+
+
+
+
+

High Trust

+

Verified members and moderated communities ensure a safe and respectful environment.

+
+
+
+
+

Value Centered

+

Match based on shared values like growth, community, and adventure.

+
+
+
+
+

Intent Driven

+

Clearly state what you're looking for: networking, friendship, or activity partners.

+
+
+
+
+
+ +
+
+

Ready to find your community?

+ Join CommonGround +
+
+{% endblock %} diff --git a/core/templates/core/chat.html b/core/templates/core/chat.html new file mode 100644 index 0000000..b927301 --- /dev/null +++ b/core/templates/core/chat.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% block title %}Chat with {{ partner.first_name }} | CommonGround{% endblock %} + +{% block content %} +
+
+
+
+
+ + {{ partner.username }} +
+
{{ partner.get_full_name|default:partner.username }}
+ Active now +
+
+ +
+ {% for message in chat_messages %} +
+
+

{{ message.body }}

+ + {{ message.timestamp|date:"H:i" }} + +
+
+ {% empty %} +
+

No messages yet. Say hello to {{ partner.first_name }}!

+
+ {% endfor %} +
+ + +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/edit_profile.html b/core/templates/core/edit_profile.html new file mode 100644 index 0000000..f5d718f --- /dev/null +++ b/core/templates/core/edit_profile.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

Edit Profile

+ +
+ {% csrf_token %} + +
+
+ Current Avatar + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancel +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/inbox.html b/core/templates/core/inbox.html new file mode 100644 index 0000000..83f5695 --- /dev/null +++ b/core/templates/core/inbox.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block title %}Messages | CommonGround{% endblock %} + +{% block content %} +
+
+ +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..7a9db2a 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,302 @@ {% extends "base.html" %} +{% load static %} +{% load social_filters %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}Dashboard | CommonGround{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +{% if user.is_authenticated %} +
+
+ + + + +
+ +
+

Welcome back, {{ user.first_name|default:user.username }}

+
+ {{ user.profile.get_transition_status_display }} + {{ user.profile.location_city|default:"Location not set" }} +
+
+ + +
+
+
+ {% csrf_token %} +
+ +
+ + +
+
+
+
+ +
+ + +
+
+ +
+
+
+
+ + +
+
+
Suggested Members
+ View All +
+
+ {% for profile in suggested_members %} + + {% endfor %} +
+
+ + +
+
Activity Feed
+ {% for post in posts %} +
+
+ {{ post.get_post_type_display }} +
+
+ +
+

{{ post.author.get_full_name|default:post.author.username }}

+

{{ post.timestamp|timesince }} ago

+
+
+ +
+

{{ post.content }}

+ {% if post.image %} + + {% endif %} + + +
+ + {{ post.reactions.count }} + + +
+ + +
+
+ {% for comment in post.comments.all %} +
+ +
+
+

{{ comment.author.username }}

+

{{ comment.content }}

+
+
+
+ {% endfor %} + +
+ {% csrf_token %} +
+ + +
+
+
+
+
+
+ {% empty %} +
+

No posts yet. Start the conversation!

+
+ {% endfor %} +
+
+ + +
+ +
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file + + +{% else %} + +
+
+
+
+

Intentional connections for life's transitions.

+

CommonGround helps professionals build high-trust, platonic relationships based on shared values and life goals.

+ +
+
+
+
+ +
+
+
+
+

Discovery Feed

+

Members aligned with current goals and locations.

+
+
+ All Aligned + {% for intent in intents %} + {{ intent.name }} + {% endfor %} +
+
+ +
+ {% for profile in profiles %} +
+
+
+ {{ profile.user.get_full_name }} +
+ {{ profile.get_transition_status_display }} +

{{ profile.user.first_name }} {{ profile.user.last_name|slice:":1" }}.

+

{{ profile.location_city }}

+
+
+

{{ profile.professional_headline }}

+

{{ profile.bio }}

+ +
+ {% for intent in profile.intents.all %} + {{ intent.name }} + {% endfor %} +
+ + +
+
+ {% empty %} +
+

No profiles found yet.

+
+ {% endfor %} +
+
+
+{% endif %} +{% endblock %} diff --git a/core/templates/core/onboarding.html b/core/templates/core/onboarding.html new file mode 100644 index 0000000..6bd7f04 --- /dev/null +++ b/core/templates/core/onboarding.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}Onboarding - CommonGround{% endblock %} + +{% block content %} +
+
+
+

Welcome to CommonGround

+

Let's set up your profile to help you find the right connections.

+
+
+ +
+
+
+
+ {% csrf_token %} +
+ + +
What do you do? (Keep it professional and clear)
+
+ +
+ + +
+ + +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/profile_detail.html b/core/templates/core/profile_detail.html new file mode 100644 index 0000000..ba90725 --- /dev/null +++ b/core/templates/core/profile_detail.html @@ -0,0 +1,175 @@ +{% extends 'base.html' %} +{% load social_filters %} + +{% block content %} +
+
+
+
+
+
+ {{ target_user.username }} +
+
+ +
+

{{ target_user.first_name }} {{ target_user.last_name }}

+

@{{ target_user.username }}

+ + {% if target_user.profile.professional_headline %} +
{{ target_user.profile.professional_headline }}
+ {% endif %} + +
+ + {{ target_user.profile.location_city|default:"Location not set" }} + + + {{ target_user.profile.get_transition_status_display }} + +
+ +
+ {{ target_user.profile.bio|linebreaks }} +
+ + {% if request.user == target_user %} + + {% else %} +
+ {% if is_following %} + + Following + + {% else %} + + Follow + + {% endif %} + + Message + +
+ {% endif %} +
+ + +
+
+
+
+

Recent Activity

+ {% for post in target_user.posts.all %} +
+
+ {{ post.get_post_type_display }} +
+
+ +
+

{{ post.author.get_full_name|default:post.author.username }}

+

{{ post.timestamp|timesince }} ago

+
+
+ {% if post.author == user %} + + {% endif %} +
+

{{ post.content }}

+ {% if post.image %} + + {% endif %} + +
+ + {{ post.reactions.count }} + + +
+ +
+
+ {% for comment in post.comments.all %} +
+ +
+
+

{{ comment.author.username }}

+

{{ comment.content }}

+
+
+
+ {% endfor %} +
+
+
+
+ {% empty %} +
+

No posts yet.

+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html new file mode 100644 index 0000000..a1e1039 --- /dev/null +++ b/core/templates/core/settings.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block title %}Settings - CommonGround{% endblock %} + +{% block content %} +
+
+
+
+

Account Settings

+ +
+
+
+
+

Preferences

+
+ {% csrf_token %} +
+ +
+ + +
+
+ +
+ + {% if profile.is_email_verified %} + Verified + {% else %} + Pending +

Verification is simulated for MVP.

+ {% endif %} +
+ + +
+
+
+
+
+{% endblock %} diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..5f8b0a6 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}Log In - CommonGround{% endblock %} + +{% block content %} +
+
+
+
+

Welcome back

+
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+

Don't have an account? Sign up

+
+
+
+
+
+{% endblock %} diff --git a/core/templates/registration/signup.html b/core/templates/registration/signup.html new file mode 100644 index 0000000..a1a44c1 --- /dev/null +++ b/core/templates/registration/signup.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block title %}Sign Up - CommonGround{% endblock %} + +{% block content %} +
+
+
+
+

Create your account

+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+

Already have an account? Log in

+
+
+
+
+
+{% endblock %} + +{% block head %} + +{% endblock %} diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/templatetags/__pycache__/__init__.cpython-311.pyc b/core/templatetags/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..15938f1 Binary files /dev/null and b/core/templatetags/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/templatetags/__pycache__/social_filters.cpython-311.pyc b/core/templatetags/__pycache__/social_filters.cpython-311.pyc new file mode 100644 index 0000000..68ee537 Binary files /dev/null and b/core/templatetags/__pycache__/social_filters.cpython-311.pyc differ diff --git a/core/templatetags/social_filters.py b/core/templatetags/social_filters.py new file mode 100644 index 0000000..400306d --- /dev/null +++ b/core/templatetags/social_filters.py @@ -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 diff --git a/core/urls.py b/core/urls.py index 6299e3d..5acc588 100644 --- a/core/urls.py +++ b/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//", chat_detail, name="chat_detail"), + path("profile/", profile_view, name="my_profile"), + path("profile/edit/", edit_profile, name="edit_profile"), + path("profile//", profile_detail, name="profile_detail"), + path("profile//follow/", toggle_follow, name="toggle_follow"), + + # Social URLs + path("post/create/", create_post, name="create_post"), + path("post//delete/", delete_post, name="delete_post"), + path("post//comment/", add_comment, name="add_comment"), + path("post//react/", toggle_reaction, name="toggle_reaction"), + path("post//hide/", hide_post, name="hide_post"), + + # Auth URLs + path("accounts/", include("django.contrib.auth.urls")), ] diff --git a/core/views.py b/core/views.py index c9aed12..4574534 100644 --- a/core/views.py +++ b/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}) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..7bda0c8 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -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; }