Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 📋 What does this PR do?
<!-- Describe your changes in 1-2 sentences -->

## 🔗 Related Issue
Closes #

## 🧪 Checklist
- [ ] Code works locally
- [ ] Acceptance criteria tested and passing
- [ ] No console errors
- [ ] Responsive on mobile and desktop
- [ ] No unnecessary files committed (node_modules, .env)

## 📸 Screenshots (if UI changed)
<!-- Drag and drop a screenshot here -->
84 changes: 84 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: CI Pipeline

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
frontend:
name: Frontend Checks (React)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install frontend dependencies
run: |
cd frontend
npm install

- name: Check for errors
run: |
cd frontend
npm run build

backend:
name: Backend Checks (Django)
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: swappit
POSTGRES_PASSWORD: swappit
POSTGRES_DB: swappit_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install backend dependencies
run: |
cd backend
pip install -r requirements.txt

- name: Run Django checks
env:
DATABASE_URL: postgresql://swappit:swappit@localhost:5432/swappit_db
SECRET_KEY: ci-secret-key-for-testing
DEBUG: True
run: |
cd backend
python manage.py check

- name: Run migrations check
env:
DATABASE_URL: postgresql://swappit:swappit@localhost:5432/swappit_db
SECRET_KEY: ci-secret-key-for-testing
DEBUG: True
run: |
cd backend
python manage.py migrate
52 changes: 52 additions & 0 deletions App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AppProvider } from './context/AppContext';
import ProtectedRoute from './components/ProtectedRoute';

// Pages
import Landing from './pages/Landing';
import Signup from './pages/Signup';
import Signin from './pages/Signin';
import Explorer from './pages/Explorer';
import MySpace from './pages/MySpace';
import ItemDetail from './pages/ItemDetail';
import CreateItem from './pages/CreateItem';
import About from './pages/About';
import ProposerTroc from './pages/ProposerTroc';

export default function App() {
return (
<Router>
{/*
AppProvider enveloppe toute l'application :
- currentUser disponible partout via useApp()
- login() / logout() accessibles dans Signin et Navbar
*/}
<AppProvider>
<Routes>
{/* ── Pages publiques ── */}
<Route path="/" element={<Landing />} />
<Route path="/about" element={<About />} />
<Route path="/signup" element={<Signup />} />
<Route path="/signin" element={<Signin />} />

{/* ── Pages protégées ── */}
<Route path="/explorer"
element={<ProtectedRoute><Explorer /></ProtectedRoute>} />
<Route path="/myspace"
element={<ProtectedRoute><MySpace /></ProtectedRoute>} />
<Route path="/item/:id"
element={<ProtectedRoute><ItemDetail /></ProtectedRoute>} />
<Route path="/explorer/:id"
element={<ProtectedRoute><ItemDetail /></ProtectedRoute>} />
<Route path="/publier"
element={<ProtectedRoute><CreateItem /></ProtectedRoute>} />
<Route path="/proposer-troc/:targetItemId"
element={<ProtectedRoute><ProposerTroc /></ProtectedRoute>} />

{/* ── Fallback ── */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AppProvider>
</Router>
);
}
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 🔄 SWAPPIT

> A community-driven item swap platform built for Cameroon.
> Trade what you have for what you need — no money required.

---

## 📸 Screenshot
<!-- Drag and drop a screenshot of your app here after uploading -->
<img width="1909" height="954" alt="image" src="https://github.com/user-attachments/assets/4650c94f-3b40-43db-89b4-5a23fd1af0e5" />

---

## 🚀 What is Swappit?
Swappit allows registered users to post items they no longer need
and propose swaps with other users — completely free,
based on item value in FCFA.

---

## ✨ Features
- 📦 Post items with photo, description and value
- 🔍 Browse and search available items
- 🤝 Propose and accept swap offers
- ⭐ Trust scores and reviews after swaps
- 🗺️ Meeting point map for accepted swaps
- 🤖 AI value estimator and chat assistant

---

## 🛠️ Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React JS |
| Backend | Django |
| Database | PostgreSQL |
| Auth | Django Authentication |
| Hosting | Contabo |

---

## ⚙️ How to Run Locally

```bash
# Clone the repo
git clone https://github.com/ChatGPT486/SWAPPIT.git

# Navigate into the project
cd SWAPPIT

# Install dependencies
npm install

# Start the development server
npm run dev
```

---

## 👥 Team
| Name | Role | GitHub |
|---|---|---|
| Tabi Paul Agwe | Full Stack Developer | [@ChatGPT486](https://github.com/ChatGPT486) |
| Takam Serge | Full Stack Developer | [@magmus2006](https://github.com/magmus2006) |
| Obam Banga Samuel | Frontend Developer & Database Engineer | [@obamX](https://github.com/obamX) |
| Ndongo Pamsy | Frontend Developer | [@ndongopamsy08-hue](https://github.com/ndongopamsy08-hue) |
|Nzeugang Daniel| Backend Developer | [@Derthgyu](https://github.com/Derthgyu) |

---
## 📅 Project Status
| Sprint | Theme | User Stories | Points | Status |
|---|---|---|---|---|
| Sprint 1 | Core Auth & Item Posting | US-01 User can create an account | 3pts | 🔨 In Progress |
| | | US-02 User can sign in and sign out | 2pts | 🔨 In Progress |
| | | US-03 User can post an item with photo | 5pts | 🔨 In Progress |
| | | US-04 User can view and edit their profile | 3pts | 🔨 In Progress |
| Sprint 2 | Marketplace & Swap Flow | US-05 User can browse all items in Explorer | 3pts | ⏳ Upcoming |
| | | US-06 User can search and filter items | 3pts | ⏳ Upcoming |
| | | US-07 User can propose a swap with fairness indicator | 8pts | ⏳ Upcoming |
| | | US-08 Owner can accept or reject a swap proposal | 5pts | ⏳ Upcoming |
| | | US-09 Contacts are shared when swap is accepted | 3pts | ⏳ Upcoming |
| Sprint 3 | Notifications & Reviews | US-10 User receives notifications for proposals | 5pts | ⏳ Upcoming |
| | | US-11 User can leave a star review after a swap | 5pts | ⏳ Upcoming |
| | | US-12 User trust score is visible on their items | 2pts | ⏳ Upcoming |
| | | US-13 Smart suggestions shown in Explorer | 5pts | ⏳ Upcoming |
| Sprint 4 | Map, AI & Polish | US-14 AI estimates item value automatically | 5pts | ⏳ Upcoming |
| | | US-15 AI chat assistant answers swap questions | 8pts | ⏳ Upcoming |
| | | US-16 Map shows meeting point for accepted swaps | 8pts | ⏳ Upcoming |
| | | US-17 About page shows team and mission | 2pts | ⏳ Upcoming |

**Total: 74 story points across 4 sprints**
---

## 📄 License
This project is for educational purposes.


Empty file added backend/core/__init__.py
Empty file.
Binary file added backend/core/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added backend/core/__pycache__/admin.cpython-313.pyc
Binary file not shown.
Binary file added backend/core/__pycache__/apps.cpython-313.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added backend/core/__pycache__/views.cpython-313.pyc
Binary file not shown.
27 changes: 27 additions & 0 deletions backend/core/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.contrib import admin
from .models import User, Item, SwapExchange

# 1. PERSONNALISATION DE L'AFFICHAGE DES UTILISATEURS
class UserAdmin(admin.ModelAdmin):
list_display = ('id', 'username', 'email', 'first_name', 'last_name', 'contact', 'created_at')
search_fields = ('username', 'email', 'first_name', 'last_name')
ordering = ('-created_at',)

# 2. PERSONNALISATION DE L'AFFICHAGE DES ARTICLES (ITEMS)
class ItemAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'user', 'category', 'condition', 'value', 'created_at')
list_filter = ('category', 'condition', 'created_at')
search_fields = ('title', 'description', 'user__username')
# On affiche 'value' car on l'a sécurisé avec default=0, null=True, blank=True
fields = ('user', 'title', 'description', 'category', 'condition', 'value', 'emoji', 'image')

# 3. PERSONNALISATION DE L'AFFICHAGE DES ÉCHANGES (SWAPS)
class SwapExchangeAdmin(admin.ModelAdmin):
list_display = ('id', 'sender', 'receiver', 'my_item', 'their_item', 'status', 'fairness', 'created_at')
list_filter = ('status', 'fairness', 'created_at')
search_fields = ('sender__username', 'receiver__username', 'my_item__title', 'their_item__title')

# Enregistrement de tous les modèles avec leurs configurations respectives
admin.site.register(User, UserAdmin)
admin.site.register(Item, ItemAdmin)
admin.site.register(SwapExchange, SwapExchangeAdmin)
5 changes: 5 additions & 0 deletions backend/core/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CoreConfig(AppConfig):
name = 'core'
78 changes: 78 additions & 0 deletions backend/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Generated by Django 6.0.5 on 2026-06-02 09:31

import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]

operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('contact', models.CharField(blank=True, max_length=20, null=True)),
('bio', models.TextField(blank=True, null=True)),
('avatar_color', models.CharField(default='#E8521F', max_length=7)),
('created_at', models.DateTimeField(auto_now_add=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField()),
('category', models.CharField(choices=[('Electronics', 'Electronics'), ('Clothing', 'Clothing'), ('Furniture', 'Furniture'), ('Books', 'Books'), ('Music', 'Music'), ('Sports', 'Sports'), ('Other', 'Other')], default='Other', max_length=50)),
('condition', models.CharField(choices=[('NEW', 'Neuf'), ('LIKE_NEW', 'Très bon état'), ('USED', 'Utilisé')], default='USED', max_length=20)),
('value', models.IntegerField(blank=True, default=0, null=True)),
('emoji', models.CharField(default='📦', max_length=10)),
('image', models.ImageField(blank=True, null=True, upload_to='items/')),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='SwapExchange',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='pending', max_length=20)),
('fairness', models.CharField(choices=[('fair', 'Fair'), ('unfair', 'Unfair'), ('skewed', 'Skewed')], default='fair', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('my_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='initiated_swaps', to='core.item')),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_swaps', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_swaps', to=settings.AUTH_USER_MODEL)),
('their_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targeted_swaps', to='core.item')),
],
),
]
18 changes: 18 additions & 0 deletions backend/core/migrations/0002_alter_item_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-06-02 18:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='item',
name='image',
field=models.URLField(blank=True, null=True),
),
]
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading