Autosave: 20260217-172745

This commit is contained in:
Flatlogic Bot 2026-02-17 17:27:46 +00:00
parent 37aee45705
commit a08f77aa00
44 changed files with 2084 additions and 172 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
# Generated by Django 5.2.7 on 2026-02-17 16:40
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Intent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('icon', models.CharField(blank=True, help_text='Bootstrap icon class name', max_length=50)),
],
),
migrations.CreateModel(
name='ValueTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('professional_headline', models.CharField(blank=True, max_length=255)),
('transition_status', models.CharField(choices=[('none', 'Stable'), ('post-divorce', 'Post-Divorce'), ('relocating', 'Relocating'), ('career-change', 'Career Change'), ('new-in-town', 'New in Town')], default='none', max_length=50)),
('bio', models.TextField(blank=True)),
('location_city', models.CharField(blank=True, max_length=100)),
('avatar_url', models.URLField(blank=True, null=True)),
('intents', models.ManyToManyField(blank=True, to='core.intent')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('value_tags', models.ManyToManyField(blank=True, to='core.valuetag')),
],
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 5.2.7 on 2026-02-17 16:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_email_verified',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='profile',
name='onboarding_completed',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='profile',
name='organization_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='profile',
name='two_factor_enabled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='profile',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 5.2.7 on 2026-02-17 16:49
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_profile_is_email_verified_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Group',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField()),
('is_private', models.BooleanField(default=False)),
('organization_id', models.IntegerField(blank=True, null=True)),
('members', models.ManyToManyField(related_name='joined_groups', to=settings.AUTH_USER_MODEL)),
('moderators', models.ManyToManyField(related_name='moderated_groups', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField()),
('start_time', models.DateTimeField()),
('end_time', models.DateTimeField()),
('location', models.CharField(max_length=255)),
('organization_id', models.IntegerField(blank=True, null=True)),
('attendees', models.ManyToManyField(related_name='attending_events', to=settings.AUTH_USER_MODEL)),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='core.group')),
],
),
migrations.CreateModel(
name='Report',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('resolved', models.BooleanField(default=False)),
('organization_id', models.IntegerField(blank=True, null=True)),
('reported_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reports_received', to=settings.AUTH_USER_MODEL)),
('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports_made', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 5.2.7 on 2026-02-17 16:51
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_group_event_report'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('is_read', models.BooleanField(default=False)),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['timestamp'],
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 5.2.7 on 2026-02-17 17:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_message'),
]
operations = [
migrations.RemoveField(
model_name='profile',
name='avatar_url',
),
migrations.AddField(
model_name='profile',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='avatars/'),
),
]

View File

@ -0,0 +1,65 @@
# Generated by Django 5.2.7 on 2026-02-17 17:06
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_remove_profile_avatar_url_profile_avatar'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('image', models.ImageField(blank=True, null=True, upload_to='posts/')),
('timestamp', models.DateTimeField(auto_now_add=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.post')),
],
options={
'ordering': ['timestamp'],
},
),
migrations.CreateModel(
name='HiddenPost',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hidden_posts', to=settings.AUTH_USER_MODEL)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.post')),
],
options={
'unique_together': {('user', 'post')},
},
),
migrations.CreateModel(
name='Reaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reaction_type', models.CharField(choices=[('heart', 'Heart'), ('like', 'Like'), ('smile', 'Smile')], default='heart', max_length=20)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='core.post')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('post', 'user', 'reaction_type')},
},
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 5.2.7 on 2026-02-17 17:17
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_post_comment_hiddenpost_reaction'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='post',
name='post_type',
field=models.CharField(choices=[('reflection', 'Reflection'), ('looking_for', 'Looking For'), ('offering', 'Offering'), ('event_invite', 'Event Invite'), ('progress_update', 'Progress Update'), ('skill_share', 'Skill Share')], default='reflection', max_length=20),
),
migrations.AddField(
model_name='profile',
name='accountability_streak',
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name='Connection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('user1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections1', to=settings.AUTH_USER_MODEL)),
('user2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections2', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user1', 'user2')},
},
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-17 17:22
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_post_post_type_profile_accountability_streak_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Follow',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('followed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_relations', to=settings.AUTH_USER_MODEL)),
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_relations', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('follower', 'followed')},
},
),
]

View File

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

View File

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

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

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

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

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

View File

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

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

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

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

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

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

View File

Binary file not shown.

View 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

View File

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

View File

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

View File

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