diff --git a/Planteer/Planteer/__init__.py b/Planteer/Planteer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/Planteer/asgi.py b/Planteer/Planteer/asgi.py new file mode 100644 index 0000000..7693f2d --- /dev/null +++ b/Planteer/Planteer/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for Planteer project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Planteer.settings') + +application = get_asgi_application() diff --git a/Planteer/Planteer/settings.py b/Planteer/Planteer/settings.py new file mode 100644 index 0000000..4c4c915 --- /dev/null +++ b/Planteer/Planteer/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for Planteer project. + +Generated by 'django-admin startproject' using Django 6.0.4. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-p^&t_=40ah+m*27wv-@q0shitg%&%t*trdg4)*d8f#=7@2h@q-' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'main', + 'plants', + 'contact', + 'accounts', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'Planteer.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'Planteer.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') \ No newline at end of file diff --git a/Planteer/Planteer/urls.py b/Planteer/Planteer/urls.py new file mode 100644 index 0000000..85935cb --- /dev/null +++ b/Planteer/Planteer/urls.py @@ -0,0 +1,28 @@ +""" +URL configuration for Planteer project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include("main.urls")), + path('plants/', include("plants.urls")), + path('contact/', include('contact.urls')), + path('accounts/', include('accounts.urls')), +]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/Planteer/Planteer/wsgi.py b/Planteer/Planteer/wsgi.py new file mode 100644 index 0000000..f14ac6f --- /dev/null +++ b/Planteer/Planteer/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for Planteer project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Planteer.settings') + +application = get_wsgi_application() diff --git a/Planteer/accounts/__init__.py b/Planteer/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/accounts/admin.py b/Planteer/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Planteer/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Planteer/accounts/apps.py b/Planteer/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/Planteer/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/Planteer/accounts/migrations/__init__.py b/Planteer/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/accounts/models.py b/Planteer/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/Planteer/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/Planteer/accounts/templates/accounts/log_out.html b/Planteer/accounts/templates/accounts/log_out.html new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/accounts/templates/accounts/signin.html b/Planteer/accounts/templates/accounts/signin.html new file mode 100644 index 0000000..62b978b --- /dev/null +++ b/Planteer/accounts/templates/accounts/signin.html @@ -0,0 +1,36 @@ +{% extends "main/base.html" %} + +{% block content %} +
+
+
+

SIGN IN

+

WELCOME BACK TO PLANTEER SYSTEM

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

+ NOT A MEMBER? JOIN NOW +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/accounts/templates/accounts/signup.html b/Planteer/accounts/templates/accounts/signup.html new file mode 100644 index 0000000..c2bc9bc --- /dev/null +++ b/Planteer/accounts/templates/accounts/signup.html @@ -0,0 +1,51 @@ +{% extends "main/base.html" %} + +{% block content %} +
+
+
+

JOIN PLANTEER

+

Create your botanical profile

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

+ ALREADY A MEMBER? SIGN IN +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/accounts/tests.py b/Planteer/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/accounts/urls.py b/Planteer/accounts/urls.py new file mode 100644 index 0000000..0d37818 --- /dev/null +++ b/Planteer/accounts/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = 'accounts' + +urlpatterns = [ + path("sign_up/",views.sign_up,name="sign_up"), + path("signin/", views.sign_in, name="sign_in"), + path("logout/", views.log_out, name="log_out"), +] \ No newline at end of file diff --git a/Planteer/accounts/views.py b/Planteer/accounts/views.py new file mode 100644 index 0000000..5f9b5b6 --- /dev/null +++ b/Planteer/accounts/views.py @@ -0,0 +1,52 @@ +from django.shortcuts import render, redirect +from django.http import HttpRequest,HttpResponse +from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login, logout +from django.contrib import messages + +# Create your views here. + + +def sign_up(request: HttpRequest): + if request.method == "POST": + try: + new_user = User.objects.create_user( + username=request.POST["username"], + password=request.POST["password"], + email=request.POST.get("email", ""), + first_name=request.POST.get("first_name", ""), + last_name=request.POST.get("last_name", "") + ) + + messages.success(request, "Registered User Successfully", "alert-success") + return redirect("accounts:sign_in") + + except IntegrityError: + messages.error(request, "Username already exists. Please choose another one.", "alert-danger") + except Exception as e: + print(f"Error during registration: {e}") + messages.error(request, "An error occurred. Please try again.", "alert-danger") + + return render(request, "accounts/signup.html") + +def sign_in(request: HttpRequest): + if request.method == "POST": + username_val = request.POST.get("username") + password_val = request.POST.get("password") + + user = authenticate(request, username=username_val, password=password_val) + + if user: + login(request, user) + messages.success(request, f"Welcome back, {user.username}", "alert-success") + return redirect(request.GET.get("next", "main:home_view")) + else: + messages.error(request, "Invalid credentials, please try again.", "alert-danger") + + return render(request, "accounts/signin.html") + +def log_out(request: HttpRequest): + logout(request) + messages.success(request, "Logged out successfully", "alert-warning") + + return redirect("main:home_view") \ No newline at end of file diff --git a/Planteer/contact/__init__.py b/Planteer/contact/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/contact/admin.py b/Planteer/contact/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Planteer/contact/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Planteer/contact/apps.py b/Planteer/contact/apps.py new file mode 100644 index 0000000..1f23179 --- /dev/null +++ b/Planteer/contact/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ContactConfig(AppConfig): + name = 'contact' diff --git a/Planteer/contact/migrations/0001_initial.py b/Planteer/contact/migrations/0001_initial.py new file mode 100644 index 0000000..e9138c5 --- /dev/null +++ b/Planteer/contact/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.4 on 2026-04-19 06:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ContactMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=100)), + ('last_name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/Planteer/contact/migrations/__init__.py b/Planteer/contact/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/contact/models.py b/Planteer/contact/models.py new file mode 100644 index 0000000..f677dea --- /dev/null +++ b/Planteer/contact/models.py @@ -0,0 +1,13 @@ +from django.db import models + +# Create your models here. + +class ContactMessage(models.Model): + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + email = models.EmailField() + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.first_name} {self.email}" \ No newline at end of file diff --git a/Planteer/contact/templates/contact/contact.html b/Planteer/contact/templates/contact/contact.html new file mode 100644 index 0000000..18a0442 --- /dev/null +++ b/Planteer/contact/templates/contact/contact.html @@ -0,0 +1,30 @@ +{% extends "main/base.html" %} + +{% block content %} + +
+
+
+

Contact Us

+
+ {% csrf_token %} +
+
+ +
+
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/contact/templates/contact/messages.html b/Planteer/contact/templates/contact/messages.html new file mode 100644 index 0000000..d81d839 --- /dev/null +++ b/Planteer/contact/templates/contact/messages.html @@ -0,0 +1,38 @@ +{% extends 'main/base.html' %} + +{% block content %} +
+
+
+

Inbox Messages

+ Total: {{ user_messages.count }} +
+ +
+ {% for msg in user_messages %} +
+
+
+
+
{{ msg.first_name }} {{ msg.last_name }}
+
+

+ {{ msg.email }} +

+ +

{{ msg.message }}

+
+ {{ msg.created_at|date:"Y-m-d | H:i" }} +
+
+
+
+ {% empty %} +
+

No messages received yet.

+
+ {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/contact/tests.py b/Planteer/contact/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/contact/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/contact/urls.py b/Planteer/contact/urls.py new file mode 100644 index 0000000..bad6c38 --- /dev/null +++ b/Planteer/contact/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = "contact" + +urlpatterns= [ + + path('', views.contact_view, name='contact_view'), + path('messages/', views.messages_view, name='messages_view'), +] \ No newline at end of file diff --git a/Planteer/contact/views.py b/Planteer/contact/views.py new file mode 100644 index 0000000..e407f63 --- /dev/null +++ b/Planteer/contact/views.py @@ -0,0 +1,28 @@ +from django.shortcuts import render, redirect +from .models import ContactMessage +from django.contrib import messages + +# Create your views here. +def contact_view(request): + if request.method == "POST": + data = { + 'first_name': request.POST.get('first_name'), + 'last_name': request.POST.get('last_name'), + 'email': request.POST.get('email'), + 'message': request.POST.get('message'), + } + + ContactMessage.objects.create(**data) + + messages.success(request, "تم استلام رسالتك بنجاح!") + return redirect('contact:contact_view') + + return render(request, 'contact/contact.html') + + +def messages_view(request): + all_messages = ContactMessage.objects.all().order_by('-created_at') + + return render(request, 'contact/messages.html', { + 'user_messages': all_messages + }) \ No newline at end of file diff --git a/Planteer/db.sqlite3 b/Planteer/db.sqlite3 new file mode 100644 index 0000000..f26895b Binary files /dev/null and b/Planteer/db.sqlite3 differ diff --git a/Planteer/main/__init__.py b/Planteer/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/main/admin.py b/Planteer/main/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Planteer/main/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Planteer/main/apps.py b/Planteer/main/apps.py new file mode 100644 index 0000000..833bff6 --- /dev/null +++ b/Planteer/main/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MainConfig(AppConfig): + name = 'main' diff --git a/Planteer/main/migrations/__init__.py b/Planteer/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/main/models.py b/Planteer/main/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/Planteer/main/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/Planteer/main/static/css/style.css b/Planteer/main/static/css/style.css new file mode 100644 index 0000000..820b1b3 --- /dev/null +++ b/Planteer/main/static/css/style.css @@ -0,0 +1,1049 @@ +:root { + --primary-dark: #031711; + --secondary-dark: #062A20; + --accent-gold: #E9CF8F; + --text-main: #F1F1F1; + --text-muted: #B4C3BE; + --white: #ffffff; + --transition: all 0.3s ease; + --radius-lg: 20px; + --radius-md: 12px; + --shadow: 0 15px 35px rgba(0, 0, 0, 0.5); +} + +html { + scroll-behavior: smooth; +} + +body { + background-color: var(--primary-dark); + color: var(--text-muted); + font-family: 'Inter', -apple-system, sans-serif; + line-height: 1.6; + overflow-x: hidden; + margin: 0; + padding: 0; +} + +section, .section-padding { + padding-top: 100px; + padding-bottom: 100px; + min-height: 70vh; + display: flex; + flex-direction: column; + justify-content: center; +} + +h1, h2, h3, h4, .fw-bold { + color: var(--text-main) !important; +} + +.navbar { + background-color: var(--primary-dark) !important; + border-bottom: 1px solid rgba(233, 207, 143, 0.1) !important; + padding: 1.2rem 0; +} + +.navbar-brand { + color: var(--accent-gold) !important; + letter-spacing: 2px; + font-size: 1.5rem; +} + +.nav-link { + color: var(--text-muted) !important; + transition: var(--transition); + margin: 0 10px; +} + +.nav-link:hover, .nav-link.active { + color: var(--accent-gold) !important; +} + +.custom-nav-link { + font-family: 'Poppins', sans-serif; + + font-size: 1rem; + letter-spacing: 0.5px; + color: #a0a0a0 !important; + text-decoration: none; + transition: all 0.3s ease; +} + +.navbar-nav .nav-item { + font-family: 'Poppins', sans-serif; +} +.card { + background-color: var(--secondary-dark) !important; + border: 1px solid rgba(255, 255, 255, 0.05) !important; + border-radius: var(--radius-lg) !important; + overflow: hidden; + transition: var(--transition); + height: 100%; +} + +.card:hover { + transform: translateY(-10px); + box-shadow: var(--shadow); + border-color: rgba(233, 207, 143, 0.3) !important; +} + +.card-title { + color: var(--accent-gold) !important; + font-weight: 700; +} + +.btn-dark, .btn-primary, .btn-accent { + background-color: var(--accent-gold) !important; + color: var(--primary-dark) !important; + border: none !important; + border-radius: 50px !important; + font-weight: 700 !important; + padding: 12px 30px !important; + transition: var(--transition); +} + +.btn-dark:hover { + filter: brightness(1.1); + transform: scale(1.05); +} + +.form-control { + background-color: rgba(255, 255, 255, 0.05) !important; + border: 1px solid rgba(233, 207, 143, 0.2) !important; + color: var(--white) !important; + border-radius: var(--radius-md); +} + +.form-control:focus { + background-color: rgba(255, 255, 255, 0.1) !important; + border-color: var(--accent-gold) !important; + box-shadow: none !important; +} + +.form-label { + color: var(--accent-gold); +} + +.planteer-footer { + background-color: #020d0a !important; + border-top: 1px solid rgba(233, 207, 143, 0.1); +} + +.accent-text { + color: var(--accent-gold) !important; +} + +.main-text { + color: var(--text-main) !important; +} + +.muted-text { + color: var(--text-muted) !important; +} + +.footer-link { + color: var(--text-muted); + text-decoration: none; + font-size: 0.9rem; + transition: var(--transition); +} + +.footer-link:hover { + color: var(--accent-gold); + padding-left: 5px; +} + +.footer-divider { + border-color: var(--accent-gold); + opacity: 0.1; +} + +.social-icon { + color: var(--text-main); + font-size: 1.2rem; + transition: var(--transition); + text-decoration: none; +} + +.social-icon:hover { + color: var(--accent-gold); +} + +.hero-section { + min-height: 90vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(rgba(3, 23, 17, 0.6), rgba(3, 23, 17, 0.6)); +} + +::placeholder { + color: var(--text-muted) !important; + opacity: 0.8 !important; +} + +::-webkit-input-placeholder { + color: var(--text-muted) !important; + opacity: 0.8 !important; +} + +:-ms-input-placeholder { + color: var(--text-muted) !important; + opacity: 0.8 !important; +} + +::-moz-placeholder { + color: var(--text-muted) !important; + opacity: 0.8 !important; +} + +.form-control:focus::placeholder { + color: var(--accent-gold) !important; + opacity: 0.5 !important; +} + +.hero-section { + min-height: 90vh; + display: flex; + align-items: center; + justify-content: center; + background-image: linear-gradient(rgba(3, 23, 17, 0.3), rgba(3, 23, 17, 0.3)), url('/static/images/plant_bg_imge.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + padding: 0 20px; +} + + +.search-pill { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(233, 207, 143, 0.3); + border-radius: 50px !important; + padding: 5px; + padding-left: 15px; + overflow: hidden; + box-shadow: var(--shadow); +} + +.search-pill input { + flex: 1; + background: transparent !important; + border: none !important; + color: white !important; + padding: 12px 10px; + outline: none !important; + box-shadow: none !important; +} + +.search-pill button { + background-color: var(--accent-gold) !important; + color: var(--primary-dark) !important; + border: none !important; + border-radius: 50px !important; + padding: 10px 25px; + font-weight: bold; + transition: var(--transition); +} + +.search-pill button:hover { + transform: scale(1.03); + filter: brightness(1.1); +} + +.search-pill input::placeholder { + color: var(--text-muted) !important; + opacity: 0.7; +} + +.btn-explore { + display: inline-block; + padding: 12px 35px; + background: transparent; + color: var(--accent-gold); + border: 1px solid var(--accent-gold); + border-radius: 50px; + text-decoration: none; + font-weight: 600; + letter-spacing: 1px; + transition: all 0.4s ease; + backdrop-filter: blur(5px); +} + +.btn-explore:hover { + background: var(--accent-gold); + color: var(--primary-dark); + box-shadow: 0 0 20px rgba(233, 207, 143, 0.4); + transform: translateY(-3px); +} + + +.section-title { + color: var(--accent-gold) !important; + font-size: 2.5rem; + font-weight: 800; + text-transform: capitalize; + margin-bottom: 40px; + position: relative; + display: inline-block; +} + +.section-title::after { + content: ''; + display: block; + width: 50px; + height: 3px; + background: var(--accent-gold); + margin-top: 10px; + border-radius: 2px; +} + +.form-check-input { + background-color: rgba(255, 255, 255, 0.1) !important; + border-color: var(--accent-gold) !important; + cursor: pointer; +} + +.form-check-input:checked { + background-color: var(--accent-gold) !important; + border-color: var(--accent-gold) !important; +} + +.form-check-label { + color: var(--text-main) !important; + cursor: pointer; +} + +.form-switch .form-check-input { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(233, 207, 143, 0.5)'/%3e%3c/svg%3e") !important; +} + +.form-switch .form-check-input:checked { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important; +} + +.form-check-input:focus { + box-shadow: 0 0 0 0.25rem rgba(233, 207, 143, 0.25) !important; + border-color: var(--accent-gold) !important; +} + +.details-wrapper { + min-height: 90vh; + display: flex; + align-items: center; + padding-top: 80px; + padding-bottom: 80px; +} + +.details-wrapper { + min-height: 90vh; + display: flex; + align-items: center; + padding: 100px 0; + background: radial-gradient(circle at top right, var(--secondary-dark), var(--primary-dark)); +} + +.image-frame { + border: 1px solid rgba(233, 207, 143, 0.2); + padding: 15px; + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.02); + box-shadow: var(--shadow); +} + +.image-frame img { + border-radius: var(--radius-md); + width: 100%; + object-fit: cover; +} + +.badge-custom { + display: inline-block; + padding: 5px 15px; + background: var(--secondary-dark); + color: var(--accent-gold); + border: 1px solid var(--accent-gold); + border-radius: 50px; + font-size: 0.85rem; + text-transform: uppercase; +} + +.uppercase-title { + font-size: 0.9rem; + letter-spacing: 2px; + text-transform: uppercase; + margin-bottom: 10px; +} + +.info-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(233, 207, 143, 0.1); + border-radius: var(--radius-md); + padding: 20px; +} + +.btn-outline-custom { + padding: 10px 30px; + border: 1px solid var(--accent-gold); + color: var(--accent-gold); + border-radius: 50px; + text-decoration: none; + transition: var(--transition); +} + +.btn-outline-custom:hover { + background: var(--accent-gold); + color: var(--primary-dark); +} + +.btn-delete-custom { + padding: 10px 30px; + border: 1px solid #dc3545; + color: #dc3545; + border-radius: 50px; + text-decoration: none; + transition: var(--transition); +} + +.btn-delete-custom:hover { + background: #dc3545; + color: white; +} + + +.related-section { + padding: 80px 0; + background-color: var(--primary-dark); + border-top: 1px solid rgba(233, 207, 143, 0.1); +} + +.related-title { + color: var(--accent-gold); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 2px; + margin-bottom: 40px; + font-size: 1.5rem; +} + +.related-card { + background: var(--secondary-dark); + border-radius: var(--radius-md); + overflow: hidden; + transition: var(--transition); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.related-card:hover { + transform: translateY(-10px); + border-color: var(--accent-gold); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); +} + +.related-img-container { + position: relative; + height: 200px; + overflow: hidden; +} + +.related-img-container img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.related-card:hover .related-img-container img { + transform: scale(1.1); +} + +.related-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(3, 23, 17, 0.7); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: var(--transition); +} + +.related-card:hover .related-overlay { + opacity: 1; +} + +.view-link { + color: var(--accent-gold); + text-decoration: none; + border: 1px solid var(--accent-gold); + padding: 8px 20px; + border-radius: 50px; + font-size: 0.8rem; + font-weight: 600; + transition: var(--transition); +} + +.view-link:hover { + background: var(--accent-gold); + color: var(--primary-dark); +} + +.related-body { + padding: 20px; + text-align: center; +} + +.related-body h5 { + color: var(--text-main); + margin-bottom: 5px; + font-size: 1.1rem; +} + +.related-category { + color: var(--accent-gold); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 1px; + opacity: 0.8; +} + +.contact-card { + background: rgba(255, 255, 255, 0.02); + backdrop-filter: blur(15px); + border: 1px solid rgba(233, 207, 143, 0.1); + padding: 40px; + border-radius: 20px; + box-shadow: 0 15px 35px rgba(0,0,0,0.2); +} + +.form-control-custom { + width: 100%; + background: rgba(255, 255, 255, 0.05) !important; + border: 1px solid rgba(233, 207, 143, 0.2) !important; + border-radius: 12px; + padding: 12px 20px; + color: white !important; + margin-bottom: 15px; +} + +.form-control-custom:focus { + border-color: var(--accent-gold) !important; + outline: none; +} + + +.btn-send { + background: var(--accent-gold); + color: var(--primary-dark); + border: none; + padding: 15px; + border-radius: 50px; + font-weight: bold; + transition: 0.3s; +} + +.btn-send:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(233, 207, 143, 0.3); +} + +.message-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(233, 207, 143, 0.1); + border-radius: var(--radius-md); + transition: var(--transition); +} + +.message-card:hover { + border-color: var(--accent-gold); + transform: translateY(-5px); + background: rgba(233, 207, 143, 0.03); +} + +.badge-custom { + background: var(--accent-gold); + color: var(--primary-dark); + padding: 5px 15px; + border-radius: 50px; + font-weight: bold; +} + +.custom-nav-link { + color: #a0a0a0 !important; + font-weight: 500; + transition: all 0.3s ease; + position: relative; + padding: 0.5rem 1rem; +} + +li { + list-style: none !important; +} + +.custom-nav-link { + color: #a0a0a0 !important; + font-weight: 500; + transition: all 0.3s ease; + position: relative; + padding: 0.5rem 1rem; +} + +.custom-nav-link:hover { + color: #E9CF8F !important; + transform: translateY(-2px); +} + +.active-gold { + color: #E9CF8F !important; + font-weight: 700; +} + +.active-gold::after { + content: ''; + display: block; + width: 15px; + height: 2px; + background: #E9CF8F; + margin: 0 auto; + border-radius: 10px; + margin-top: 2px; +} + +.custom-text { + color: #ffffff !important; +} + +.reviews-section { + padding: 80px 0; + border-top: 1px solid rgba(233, 207, 143, 0.1); +} + +.reviews-title { + color: var(--white); + font-size: 1.8rem; + font-weight: 700; + letter-spacing: 2px; + text-transform: uppercase; +} + +.review-card { + background: var(--secondary-dark); + border: 1px solid rgba(233, 207, 143, 0.1); + border-radius: var(--radius-md); + padding: 25px; + margin-bottom: 20px; + box-shadow: var(--shadow); + transition: var(--transition); +} + +.review-card:hover { + border-color: var(--accent-gold); + transform: translateX(8px); +} + +.review-author { + color: var(--accent-gold); + font-weight: 600; + font-size: 1.1rem; +} + +.review-date { + color: var(--text-muted); + font-size: 0.8rem; +} + +.review-text { + color: var(--text-main); + line-height: 1.8; + margin-top: 12px; +} + +.add-review-box { + background-color: var(--primary-dark); + border: 1px solid var(--secondary-dark); + padding: 40px; + border-radius: var(--radius-lg); + position: sticky; + top: 100px; + box-shadow: var(--shadow); +} + +.add-review-box h4 { + color: var(--white); + font-size: 1.4rem; + margin-bottom: 30px; +} + +.review-input { + background: transparent !important; + border: none !important; + border-bottom: 1px solid var(--text-muted) !important; + border-radius: 0 !important; + color: var(--text-main) !important; + padding: 12px 5px !important; + margin-bottom: 25px !important; + transition: var(--transition); +} + +.review-input:focus { + border-bottom-color: var(--accent-gold) !important; + box-shadow: none !important; +} + +.review-input::placeholder { + color: var(--text-muted); + opacity: 0.5; + font-size: 0.8rem; +} + +.btn-review-submit { + background-color: var(--accent-gold); + color: var(--primary-dark); + border: 1px solid var(--accent-gold); + border-radius: var(--radius-md); + padding: 15px; + width: 100%; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 2px; + transition: var(--transition); +} + +.btn-review-submit:hover { + background-color: transparent; + color: var(--accent-gold); + box-shadow: 0 0 15px rgba(233, 207, 143, 0.3); +} + + +.plant-card { + background-color: var(--secondary-dark) !important; + border-radius: var(--radius-lg); + transition: var(--transition); + overflow: hidden; +} + +.plant-card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow); +} + +.plant-card-img { + height: 200px; + object-fit: cover; +} + + +.plant-title { + color: var(--accent-gold) !important; +} + +.plant-description { + color: var(--text-muted); + font-size: 0.9rem; +} + + +.country-badge { + background-color: var(--primary-dark); + color: var(--text-main); + border: 1px solid rgba(233, 207, 143, 0.3); + padding: 6px 12px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + gap: 6px; + transition: var(--transition); +} + +.country-badge:hover { + background-color: var(--accent-gold); + color: var(--primary-dark); +} + + +.btn-discover { + border: 1.5px solid var(--accent-gold); + color: var(--accent-gold); + background: transparent; + border-radius: var(--radius-md); + font-weight: 600; + transition: var(--transition); +} + +.btn-discover:hover { + background-color: var(--accent-gold); + color: var(--primary-dark); +} + +.country-container { + padding: 60px 20px; + background-color: var(--primary-dark); + min-height: 100vh; +} + +.country-header { + text-align: center; + margin-bottom: 50px; +} + +.country-flag-img { + width: 100px; + height: 100px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--accent-gold); + box-shadow: 0 0 20px rgba(233, 207, 143, 0.2); + padding: 5px; +} + +.country-title { + color: var(--white); + margin-top: 20px; + font-weight: 800; + letter-spacing: 1px; +} + +.country-title span { + color: var(--accent-gold); +} + +.country-subtitle { + color: var(--text-muted); + font-size: 1.1rem; +} + +.plants-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 30px; +} + +.empty-state { + text-align: center; + grid-column: 1 / -1; + padding: 60px; + border: 1px dashed var(--secondary-dark); + border-radius: var(--radius-lg); + background: rgba(6, 42, 32, 0.3); +} + +.back-link { + color: var(--accent-gold); + text-decoration: none; + font-weight: 600; + font-size: 1.1rem; + transition: var(--transition); + border-bottom: 1px solid transparent; +} + +.back-link:hover { + color: var(--white); + border-bottom: 1px solid var(--accent-gold); + padding-left: 10px; +} + +.back-navigation { + display: flex; + justify-content: flex-start; + margin-bottom: 20px; +} + +.back-btn-minimal { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-muted); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + padding: 8px 16px; + border-radius: var(--radius-md); + background: rgba(233, 207, 143, 0.05); + border: 1px solid rgba(233, 207, 143, 0.1); + transition: var(--transition); +} + +.back-btn-minimal:hover { + color: var(--accent-gold); + background: rgba(233, 207, 143, 0.1); + border-color: var(--accent-gold); + transform: translateX(-5px); +} + + + + + +.form-label.fw-semibold { + color: var(--accent-gold, #c9af8f); + margin-bottom: 12px; + display: block; + letter-spacing: 0.5px; +} + + +#countryFilter { + background-color:var(--primary-dark)!important; + border: 1px solid rgba(201, 175, 143, 0.2) !important; + color:var(--text-main) !important; + border-radius: 10px; + padding: 12px 15px; + transition: all 0.3s ease; +} + +#countryFilter:focus { + border-color:var(--accent-gold)!important; + box-shadow: 0 0 10px rgba(201, 175, 143, 0.2) !important; + outline: none; +} + + +.countries-wrapper { + background-color: var(--primary-dark)!important; + border: 1px solid rgba(255, 255, 255, 0.05) !important; + max-height: 250px; + overflow-y: auto; + padding: 20px !important; + border-radius: 15px !important; +} + +.country-pill-item { + background-color: #252525 !important; + border: 1px solid rgba(201, 175, 143, 0.1) !important; + color: #ffffff; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin-bottom: 5px; +} + +.country-pill-item:hover { + border-color: #c9af8f !important; + background-color: rgba(201, 175, 143, 0.05) !important; + transform: translateY(-2px); +} + +.country-label-name { + font-size: 0.85rem; + color: #e0e0e0; +} + +.country-pill-item img { + border: 1px solid rgba(255, 255, 255, 0.1); + filter: brightness(0.9); +} + +.form-check-input { + cursor: pointer; + background-color: #333; + border-color: rgba(201, 175, 143, 0.3); +} + +.form-check-input:checked { + background-color: #c9af8f !important; + border-color: #c9af8f !important; +} + +.text-muted { + color: #888 !important; + font-size: 0.75rem; + font-style: italic; +} + +.countries-wrapper::-webkit-scrollbar { + width: 5px; +} + +.countries-wrapper::-webkit-scrollbar-track { + background: #121212; +} + +.countries-wrapper::-webkit-scrollbar-thumb { + background: #c9af8f; + border-radius: 10px; +} + +.country-link-pill { + display: inline-flex; + align-items: center; + padding: 6px 15px; + background-color: rgba(201, 175, 143, 0.05); + border: 1px solid rgba(201, 175, 143, 0.2); + border-radius: 50px; + color: #e0e0e0; + text-decoration: none; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.country-link-pill:hover { + background-color: #c9af8f; + color: #121212; + box-shadow: 0 0 15px rgba(201, 175, 143, 0.4); + transform: translateY(-2px); +} + +.country-link-pill img { + object-fit: cover; + border: 1px solid rgba(255,255,255,0.1); +} + +.accent-gold { + color: #c9af8f; +} + + +.btn-luxury-green { + display: inline-block; + padding: 10px 28px; + background-color: transparent; + color: var(--text-muted); + border: 1.5px solid var(--text-muted); + border-radius: 0; + text-decoration: none; + font-weight: 600; + font-size: 12px; + letter-spacing: 2px; + transition: all 0.4s ease; + text-transform: uppercase; + white-space: nowrap; + } + +.btn-luxury-green:hover { + color:var(--accent-gold); + border: 1.5px solid var(--accent-gold); + box-shadow: 0 4px 15px rgba(26, 93, 26, 0.2); + } + +.custom-select-tech { + min-width: 200px; + border-radius: 0 !important; + border: 1px solid #d1d1d1; + font-size: 12px; + letter-spacing: 1px; + text-transform: uppercase; + padding: 8px 12px; + color: #444; + } + + .custom-select-tech:focus { + border-color: #1a5d1a; + box-shadow: none; + } + + + + + +.add-review-box { + + + transition: none !important; + +} diff --git a/Planteer/main/static/images/plant_bg_imge.png b/Planteer/main/static/images/plant_bg_imge.png new file mode 100644 index 0000000..4e0e6f2 Binary files /dev/null and b/Planteer/main/static/images/plant_bg_imge.png differ diff --git a/Planteer/main/templates/main/base.html b/Planteer/main/templates/main/base.html new file mode 100644 index 0000000..8bfd670 --- /dev/null +++ b/Planteer/main/templates/main/base.html @@ -0,0 +1,150 @@ +{% load static %} + + + + + + {% block title %} Planteer {% endblock %} + + + + + + + + + + +{% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+{% endif %} + +
+ {% block content %} + {% endblock %} +
+ + + + + + + + \ No newline at end of file diff --git a/Planteer/main/templates/main/home.html b/Planteer/main/templates/main/home.html new file mode 100644 index 0000000..9ef79ea --- /dev/null +++ b/Planteer/main/templates/main/home.html @@ -0,0 +1,61 @@ +{% extends "main/base.html" %} +{% load static %} + +{% block content %} +
+
+

Welcome to Planteer

+

Discover the beauty of nature and manage your botanical collection.

+ +
+
+ +
+
+ + + +
+
+ +
+
+
+

Newly Added Plants

+ View All +
+ +
+ {% for plant in plants %} +
+ {% include 'plants/includes/plant_card.html' with plant=plant %} +
+ {% endfor %} +
+
+
+ +
+
+

Botanical Wonders

+ +

+ Dive into a world of curated botanical knowledge. Explore the intricate details, + unique characteristics, and the fascinating science behind nature’s most + extraordinary plant species. +

+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/main/tests.py b/Planteer/main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/main/urls.py b/Planteer/main/urls.py new file mode 100644 index 0000000..0770718 --- /dev/null +++ b/Planteer/main/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = "main" + +urlpatterns= [ + path('', views.home_view, name="home_view") +] + diff --git a/Planteer/main/views.py b/Planteer/main/views.py new file mode 100644 index 0000000..c601d6c --- /dev/null +++ b/Planteer/main/views.py @@ -0,0 +1,9 @@ +from django.shortcuts import render +from django.http import HttpRequest,HttpResponse +from plants.models import Plant + +# Create your views here. + +def home_view(request): + latest_plants = Plant.objects.all().order_by('-created_at')[:3] + return render(request, "main/home.html", {"plants": latest_plants}) \ No newline at end of file diff --git a/Planteer/manage.py b/Planteer/manage.py new file mode 100644 index 0000000..bd0a64a --- /dev/null +++ b/Planteer/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Planteer.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Planteer/media/flags/Afghanistan_flage.png b/Planteer/media/flags/Afghanistan_flage.png new file mode 100644 index 0000000..9be8219 Binary files /dev/null and b/Planteer/media/flags/Afghanistan_flage.png differ diff --git a/Planteer/media/flags/Brazil_flage.png b/Planteer/media/flags/Brazil_flage.png new file mode 100644 index 0000000..66454db Binary files /dev/null and b/Planteer/media/flags/Brazil_flage.png differ diff --git a/Planteer/media/flags/China_flage.png b/Planteer/media/flags/China_flage.png new file mode 100644 index 0000000..8a7c582 Binary files /dev/null and b/Planteer/media/flags/China_flage.png differ diff --git a/Planteer/media/flags/Egypt_flag.png b/Planteer/media/flags/Egypt_flag.png new file mode 100644 index 0000000..1bc3636 Binary files /dev/null and b/Planteer/media/flags/Egypt_flag.png differ diff --git a/Planteer/media/flags/France_flage.png b/Planteer/media/flags/France_flage.png new file mode 100644 index 0000000..aa21452 Binary files /dev/null and b/Planteer/media/flags/France_flage.png differ diff --git a/Planteer/media/flags/India_flage.png b/Planteer/media/flags/India_flage.png new file mode 100644 index 0000000..0f95152 Binary files /dev/null and b/Planteer/media/flags/India_flage.png differ diff --git a/Planteer/media/flags/Italy_flage.png b/Planteer/media/flags/Italy_flage.png new file mode 100644 index 0000000..1bcc3fc Binary files /dev/null and b/Planteer/media/flags/Italy_flage.png differ diff --git a/Planteer/media/flags/Jordan.png b/Planteer/media/flags/Jordan.png new file mode 100644 index 0000000..7fbf571 Binary files /dev/null and b/Planteer/media/flags/Jordan.png differ diff --git a/Planteer/media/flags/Kazakhstan_flage.png b/Planteer/media/flags/Kazakhstan_flage.png new file mode 100644 index 0000000..d9a5b19 Binary files /dev/null and b/Planteer/media/flags/Kazakhstan_flage.png differ diff --git a/Planteer/media/flags/Lebanon_flag.png b/Planteer/media/flags/Lebanon_flag.png new file mode 100644 index 0000000..38583f9 Binary files /dev/null and b/Planteer/media/flags/Lebanon_flag.png differ diff --git a/Planteer/media/flags/Peru_flage.png b/Planteer/media/flags/Peru_flage.png new file mode 100644 index 0000000..3b1c761 Binary files /dev/null and b/Planteer/media/flags/Peru_flage.png differ diff --git a/Planteer/media/flags/Philippines_flage.png b/Planteer/media/flags/Philippines_flage.png new file mode 100644 index 0000000..e9ee447 Binary files /dev/null and b/Planteer/media/flags/Philippines_flage.png differ diff --git a/Planteer/media/flags/Saudi_Arabia_flag.png b/Planteer/media/flags/Saudi_Arabia_flag.png new file mode 100644 index 0000000..f694798 Binary files /dev/null and b/Planteer/media/flags/Saudi_Arabia_flag.png differ diff --git a/Planteer/media/flags/Syria_flag.png b/Planteer/media/flags/Syria_flag.png new file mode 100644 index 0000000..76f558a Binary files /dev/null and b/Planteer/media/flags/Syria_flag.png differ diff --git a/Planteer/media/flags/USA_flage.png b/Planteer/media/flags/USA_flage.png new file mode 100644 index 0000000..5190360 Binary files /dev/null and b/Planteer/media/flags/USA_flage.png differ diff --git a/Planteer/media/images/Arabian_Jasmine.jpg b/Planteer/media/images/Arabian_Jasmine.jpg new file mode 100644 index 0000000..01eb45f Binary files /dev/null and b/Planteer/media/images/Arabian_Jasmine.jpg differ diff --git a/Planteer/media/images/Bougainvillea.jpg b/Planteer/media/images/Bougainvillea.jpg new file mode 100644 index 0000000..2e42c9f Binary files /dev/null and b/Planteer/media/images/Bougainvillea.jpg differ diff --git a/Planteer/media/images/Cucumber.jpg b/Planteer/media/images/Cucumber.jpg new file mode 100644 index 0000000..9479eb4 Binary files /dev/null and b/Planteer/media/images/Cucumber.jpg differ diff --git a/Planteer/media/images/Eggplant.jpg b/Planteer/media/images/Eggplant.jpg new file mode 100644 index 0000000..dbfce39 Binary files /dev/null and b/Planteer/media/images/Eggplant.jpg differ diff --git a/Planteer/media/images/Garlic.jpg b/Planteer/media/images/Garlic.jpg new file mode 100644 index 0000000..bd273e9 Binary files /dev/null and b/Planteer/media/images/Garlic.jpg differ diff --git a/Planteer/media/images/Ghaf.jpeg b/Planteer/media/images/Ghaf.jpeg new file mode 100644 index 0000000..4f31a5e Binary files /dev/null and b/Planteer/media/images/Ghaf.jpeg differ diff --git a/Planteer/media/images/Grapes.jpg b/Planteer/media/images/Grapes.jpg new file mode 100644 index 0000000..0791947 Binary files /dev/null and b/Planteer/media/images/Grapes.jpg differ diff --git a/Planteer/media/images/Lavender.jpg b/Planteer/media/images/Lavender.jpg new file mode 100644 index 0000000..f35f31f Binary files /dev/null and b/Planteer/media/images/Lavender.jpg differ diff --git a/Planteer/media/images/Peach.jpg b/Planteer/media/images/Peach.jpg new file mode 100644 index 0000000..4bbb8f0 Binary files /dev/null and b/Planteer/media/images/Peach.jpg differ diff --git a/Planteer/media/images/Sunflower.jpg b/Planteer/media/images/Sunflower.jpg new file mode 100644 index 0000000..254000c Binary files /dev/null and b/Planteer/media/images/Sunflower.jpg differ diff --git a/Planteer/media/images/Taif_Rose.jpeg b/Planteer/media/images/Taif_Rose.jpeg new file mode 100644 index 0000000..81828f8 Binary files /dev/null and b/Planteer/media/images/Taif_Rose.jpeg differ diff --git a/Planteer/media/images/Talh.jpeg b/Planteer/media/images/Talh.jpeg new file mode 100644 index 0000000..2948dc1 Binary files /dev/null and b/Planteer/media/images/Talh.jpeg differ diff --git a/Planteer/media/images/Talh_qQ2odeb.jpeg b/Planteer/media/images/Talh_qQ2odeb.jpeg new file mode 100644 index 0000000..1c2cf2e Binary files /dev/null and b/Planteer/media/images/Talh_qQ2odeb.jpeg differ diff --git a/Planteer/media/images/aluli.jpeg b/Planteer/media/images/aluli.jpeg new file mode 100644 index 0000000..4f5a980 Binary files /dev/null and b/Planteer/media/images/aluli.jpeg differ diff --git a/Planteer/media/images/apple_1.jpg b/Planteer/media/images/apple_1.jpg new file mode 100644 index 0000000..6d0877c Binary files /dev/null and b/Planteer/media/images/apple_1.jpg differ diff --git a/Planteer/media/images/broccole.jpg b/Planteer/media/images/broccole.jpg new file mode 100644 index 0000000..6b7543f Binary files /dev/null and b/Planteer/media/images/broccole.jpg differ diff --git a/Planteer/media/images/bunana.jpg b/Planteer/media/images/bunana.jpg new file mode 100644 index 0000000..24e5dc1 Binary files /dev/null and b/Planteer/media/images/bunana.jpg differ diff --git a/Planteer/media/images/carrot.jpg b/Planteer/media/images/carrot.jpg new file mode 100644 index 0000000..558c9e8 Binary files /dev/null and b/Planteer/media/images/carrot.jpg differ diff --git a/Planteer/media/images/mango.jpg b/Planteer/media/images/mango.jpg new file mode 100644 index 0000000..bb43a83 Binary files /dev/null and b/Planteer/media/images/mango.jpg differ diff --git a/Planteer/media/images/neem.jpeg b/Planteer/media/images/neem.jpeg new file mode 100644 index 0000000..acf0a9f Binary files /dev/null and b/Planteer/media/images/neem.jpeg differ diff --git a/Planteer/media/images/pineapple.jpg b/Planteer/media/images/pineapple.jpg new file mode 100644 index 0000000..f938f7d Binary files /dev/null and b/Planteer/media/images/pineapple.jpg differ diff --git a/Planteer/media/images/potato.jpg b/Planteer/media/images/potato.jpg new file mode 100644 index 0000000..9f09fdb Binary files /dev/null and b/Planteer/media/images/potato.jpg differ diff --git a/Planteer/media/images/strawberry.jpg b/Planteer/media/images/strawberry.jpg new file mode 100644 index 0000000..fe7d79b Binary files /dev/null and b/Planteer/media/images/strawberry.jpg differ diff --git a/Planteer/media/images/tomato.jpg b/Planteer/media/images/tomato.jpg new file mode 100644 index 0000000..f101bba Binary files /dev/null and b/Planteer/media/images/tomato.jpg differ diff --git a/Planteer/media/images/watermelon.jpg b/Planteer/media/images/watermelon.jpg new file mode 100644 index 0000000..b104443 Binary files /dev/null and b/Planteer/media/images/watermelon.jpg differ diff --git a/Planteer/plants/__init__.py b/Planteer/plants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/plants/admin.py b/Planteer/plants/admin.py new file mode 100644 index 0000000..52fafbd --- /dev/null +++ b/Planteer/plants/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from .models import Plant, Review, Country + +class PlantAdmin(admin.ModelAdmin): + list_display = ('name', 'category', 'is_edible', 'created_at') + + search_fields = ('name', 'about', 'used_for') + + list_filter = ('category', 'is_edible', 'created_at') + + ordering = ('-created_at',) + +class ReviewAdmin(admin.ModelAdmin): + list_display = ('user', 'plant', 'created_at') + + search_fields = ('name', 'comment') + + list_filter = ('plant', 'created_at') + +admin.site.register(Plant, PlantAdmin) +admin.site.register(Review, ReviewAdmin) +admin.site.register(Country) \ No newline at end of file diff --git a/Planteer/plants/apps.py b/Planteer/plants/apps.py new file mode 100644 index 0000000..478c3ce --- /dev/null +++ b/Planteer/plants/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PlantsConfig(AppConfig): + name = 'plants' diff --git a/Planteer/plants/forms.py b/Planteer/plants/forms.py new file mode 100644 index 0000000..12c4a2a --- /dev/null +++ b/Planteer/plants/forms.py @@ -0,0 +1,25 @@ +from django import forms +from .models import Plant + +class PlantForm(forms.ModelForm): + name = forms.CharField( + min_length=3, + widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Plant Name'}) + ) + about = forms.CharField( + min_length=20, + widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': 'Write at least 20 characters...'}) + ) + + class Meta: + model = Plant + fields = "__all__" + + widgets = { + 'used_for': forms.Textarea(attrs={'class': 'form-control', 'rows': 2, 'placeholder': 'Usage details...'}), + 'category': forms.Select(attrs={'class': 'form-select'}), + 'is_edible': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'image': forms.FileInput(attrs={'class': 'form-control'}), + 'countries': forms.MultipleHiddenInput(), + + } \ No newline at end of file diff --git a/Planteer/plants/migrations/0001_initial.py b/Planteer/plants/migrations/0001_initial.py new file mode 100644 index 0000000..e4073b2 --- /dev/null +++ b/Planteer/plants/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.4 on 2026-04-17 13:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Plant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('about', models.TextField()), + ('used_for', models.TextField()), + ('image', models.ImageField(default='images/default.jpg', upload_to='images/')), + ('category', models.CharField(choices=[('Tree', 'Tree'), ('Fruit', 'Fruit'), ('Vegetable', 'Vegetable'), ('Flower', 'Flower')], default='Tree', max_length=50)), + ('is_edible', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/Planteer/plants/migrations/0002_review.py b/Planteer/plants/migrations/0002_review.py new file mode 100644 index 0000000..7a41272 --- /dev/null +++ b/Planteer/plants/migrations/0002_review.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.4 on 2026-04-20 08:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('plant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='plants.plant')), + ], + ), + ] diff --git a/Planteer/plants/migrations/0003_country_plant_countries.py b/Planteer/plants/migrations/0003_country_plant_countries.py new file mode 100644 index 0000000..b835f4a --- /dev/null +++ b/Planteer/plants/migrations/0003_country_plant_countries.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.4 on 2026-04-21 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0002_review'), + ] + + operations = [ + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('flag', models.ImageField(upload_to='flags/')), + ], + options={ + 'verbose_name_plural': 'Countries', + }, + ), + migrations.AddField( + model_name='plant', + name='countries', + field=models.ManyToManyField(related_name='plants', to='plants.country'), + ), + ] diff --git a/Planteer/plants/migrations/0004_remove_review_name_review_user.py b/Planteer/plants/migrations/0004_remove_review_name_review_user.py new file mode 100644 index 0000000..f6c6106 --- /dev/null +++ b/Planteer/plants/migrations/0004_remove_review_name_review_user.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.4 on 2026-04-26 16:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0003_country_plant_countries'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='review', + name='name', + ), + migrations.AddField( + model_name='review', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/Planteer/plants/migrations/__init__.py b/Planteer/plants/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/plants/models.py b/Planteer/plants/models.py new file mode 100644 index 0000000..46895a3 --- /dev/null +++ b/Planteer/plants/models.py @@ -0,0 +1,46 @@ +from django.db import models +from django.contrib.auth.models import User + +# Create your models here. + +class Country(models.Model): + name = models.CharField(max_length=100) + flag = models.ImageField(upload_to='flags/') + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "Countries" + +class Plant(models.Model): + + class Category(models.TextChoices): + TREE = "Tree", "Tree" + FRUIT = "Fruit", "Fruit" + VEGETABLE = "Vegetable", "Vegetable" + FLOWER = "Flower", "Flower" + + name = models.CharField(max_length=255) + about = models.TextField() + used_for = models.TextField() + image = models.ImageField(upload_to="images/", default="images/default.jpg") + + category = models.CharField( + max_length=50, + choices=Category.choices, + default=Category.TREE + ) + + is_edible = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + countries = models.ManyToManyField(Country, related_name='plants') + + def __str__(self): + return self.name + +class Review(models.Model): + plant = models.ForeignKey(Plant, on_delete=models.CASCADE, related_name="reviews") + user = models.ForeignKey(User, on_delete=models.CASCADE) + comment = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/Planteer/plants/templates/plants/add_plant.html b/Planteer/plants/templates/plants/add_plant.html new file mode 100644 index 0000000..138f057 --- /dev/null +++ b/Planteer/plants/templates/plants/add_plant.html @@ -0,0 +1,92 @@ +{% extends "main/base.html" %} + +{% block content %} +
+
+
+
+

+ {% if plant %} Update {{ plant.name }} {% else %} Add New Plant {% endif %} +

+ +
+ {% csrf_token %} + + {% for field in form %} +
+ + {% if field.name == 'is_edible' %} + {{ field }} + + {% else %} + + + {% if field.name == 'image' and plant.image %} +
+ +
+ {% endif %} + + {{ field }} + {% endif %} + + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + +
+ + + + +
+
    + {% for country in countries %} +
  • + + +
  • + {% endfor %} +
+
+ Filter and select the countries where this plant grows. +
+ + +
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/all_plants.html b/Planteer/plants/templates/plants/all_plants.html new file mode 100644 index 0000000..3f0f588 --- /dev/null +++ b/Planteer/plants/templates/plants/all_plants.html @@ -0,0 +1,41 @@ +{% extends "main/base.html" %} +{% load static %} + +{% block content %} +
+ +
+ +
+ +
+ {% if request.user.is_authenticated and request.user.is_staff %} + + ADD NEW PLANT + + {% endif %} + +
+ +
+ {% for plant in plants %} +
+ {% include 'plants/includes/plant_card.html' with plant=plant %} +
+ {% empty %} +
+

NO PLANTS FOUND IN THIS CATEGORY.

+
+ {% endfor %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/country_plants.html b/Planteer/plants/templates/plants/country_plants.html new file mode 100644 index 0000000..c5105f4 --- /dev/null +++ b/Planteer/plants/templates/plants/country_plants.html @@ -0,0 +1,34 @@ +{% extends 'main/base.html' %} + +{% block content %} +
+
+ + + +
+ {{ country.name }} +

Native Plants in {{ country.name }}

+

Botanical collection of this region.

+
+ +
+ {% for plant in plants %} + {% include 'plants/includes/plant_card.html' with plant=plant %} + {% empty %} +
+

No records found for this country.

+
+ {% endfor %} +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/includes/plant_card.html b/Planteer/plants/templates/plants/includes/plant_card.html new file mode 100644 index 0000000..fbeafa1 --- /dev/null +++ b/Planteer/plants/templates/plants/includes/plant_card.html @@ -0,0 +1,33 @@ +{% load static %} +
+ {% if plant.image %} + {{ plant.name }} + {% else %} + default + {% endif %} + +
+
{{ plant.name }}
+ +
+ {% for country in plant.countries.all %} + + + + {{ country.name }} + + + {% endfor %} +
+ +

+ {{ plant.about|truncatechars:80 }} +

+
+ + +
\ No newline at end of file diff --git a/Planteer/plants/templates/plants/plant_detail.html b/Planteer/plants/templates/plants/plant_detail.html new file mode 100644 index 0000000..4f1e6b9 --- /dev/null +++ b/Planteer/plants/templates/plants/plant_detail.html @@ -0,0 +1,131 @@ +{% extends "main/base.html" %} + +{% block title %} {{ plant.name }} - Details {% endblock %} + +{% block content %} +
+
+
+
+ {% if plant.image %} +
+ {{ plant.name }} +
+ {% endif %} +
+ +
+

{{ plant.name }}

+ {{ plant.category }} + +
+
Native Regions:
+
+ {% for country in plant.countries.all %} + + {{ country.name }} + {{ country.name }} + + {% empty %} + No native regions recorded. + {% endfor %} +
+
+ + + +
About:
+

{{ plant.about }}

+ +
Used for:
+

{{ plant.used_for }}

+ +
+ Edible: + {% if plant.is_edible %} + Yes, safe to eat + {% else %} + No, not for consumption + {% endif %} +
+ {% if request.user.is_authenticated and request.user.is_staff %} + + {% endif %} +
+
+
+
+ +
+
+

Community Insights

+ {% for review in plant.reviews.all %} +
+
+ {{ review.user.username }} + {{ review.created_at|date:"d M Y" }} +
+

{{ review.comment }}

+
+ {% empty %} +

No insights shared yet.

+ {% endfor %} +
+ +
+
+

Leave a Note

+ + {% if request.user.is_authenticated %} +
+ {% csrf_token %} + + + + + +
+ {% else %} +
+

Want to share your experience?

+ SIGN IN TO POST +
+ {% endif %} +
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/search.html b/Planteer/plants/templates/plants/search.html new file mode 100644 index 0000000..1344f7a --- /dev/null +++ b/Planteer/plants/templates/plants/search.html @@ -0,0 +1,54 @@ +{% extends "main/base.html" %} + +{% block content %} +
+ +
+

Search Results

+ +
+ Found {{ results|length }} results +
+ + {% if request.GET.search %} +

+ You searched for: "{{ request.GET.search }}" +

+ {% endif %} +
+ +
+ +
+ {% if results %} + {% for plant in results %} +
+
+ {% if plant.image %} + {{ plant.name }} + {% endif %} +
+
{{ plant.name }}
+

{{ plant.about|truncatewords:15 }}

+
+ View Details + {{ plant.category }} +
+
+
+
+ {% endfor %} + {% else %} +
+
+ +
+

No results found.

+

Please make sure you entered at least 2 characters.

+ Search Again +
+ {% endif %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/update_plant.html b/Planteer/plants/templates/plants/update_plant.html new file mode 100644 index 0000000..dff18f6 --- /dev/null +++ b/Planteer/plants/templates/plants/update_plant.html @@ -0,0 +1,92 @@ +{% extends "main/base.html" %} + +{% block content %} +
+
+
+
+

+ {% if plant %} Update {{ plant.name }} {% else %} Add New Plant {% endif %} +

+ +
+ {% csrf_token %} + + {% for field in form %} + {% if field.name != 'countries' %} +
+ + {% if field.name == 'is_edible' %} + {{ field }} + + {% else %} + + + {% if field.name == 'image' and plant.image %} +
+ +
+ {% endif %} + + {{ field }} + {% endif %} + + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ + + + +
+
    + {% for country in countries %} +
  • + + + +
  • + {% endfor %} +
+
+ Filter and select the countries where this plant grows. +
+ + + +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/tests.py b/Planteer/plants/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/plants/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/plants/urls.py b/Planteer/plants/urls.py new file mode 100644 index 0000000..c8d613f --- /dev/null +++ b/Planteer/plants/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from . import views + +app_name = "plants" + +urlpatterns= [ + + path("all/", views.all_plants_view, name="all_plants_view"), + path("/detail/", views.plant_detail_view, name="plant_detail_view"), + path("new/", views.add_plant_view, name="add_plant_view"), + path("/update/", views.update_plant_view, name="update_plant_view"), + path("/delete/", views.delete_plant_view, name="delete_plant_view"), + path("search/", views.search_view, name="search_view"), + path("review/add//", views.add_review_view, name="add_review_view"), + path('country//', views.country_plants_view, name='country_plants_view'), +] diff --git a/Planteer/plants/views.py b/Planteer/plants/views.py new file mode 100644 index 0000000..c701372 --- /dev/null +++ b/Planteer/plants/views.py @@ -0,0 +1,116 @@ +from django.shortcuts import render,redirect, get_object_or_404 +from django.http import HttpRequest +from .models import Plant, Review, Country +from django.contrib import messages +from .forms import PlantForm + +# Create your views here. + +def all_plants_view(request): + + all_countries = Country.objects.all() + + country_id = request.GET.get('country_filter') + + if country_id: + plants = Plant.objects.filter(countries__id=country_id) + else: + plants = Plant.objects.all() + + return render(request, "plants/all_plants.html", { + "plants": plants, + "all_countries": all_countries + }) + +def plant_detail_view(request, plant_id): + plant = Plant.objects.get(id=plant_id) + + related_plants = Plant.objects.filter(category=plant.category).exclude(id=plant.id)[:4] + + return render(request, 'plants/plant_detail.html', { + 'plant': plant, + 'related_plants': related_plants + }) + +from .models import Plant, Country + +def add_plant_view(request): + + if not request.user.is_authenticated or not request.user.is_staff: + messages.error(request, "Access Denied: This page is restricted to staff members only.", "alert-danger") + return redirect("main:home_view") + + countries = Country.objects.all() + if request.method == "POST": + form = PlantForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + return redirect("plants:all_plants_view") + else: + form = PlantForm() + return render(request, "plants/add_plant.html", {"form": form, "countries": countries}) + +def update_plant_view(request, plant_id): + + if not request.user.is_authenticated or not request.user.is_staff: + messages.error(request, "Access Denied: You do not have permission to edit plant data.", "alert-danger") + return redirect("plants:plant_detail_view", plant_id=plant_id) + plant = get_object_or_404(Plant, id=plant_id) + countries = Country.objects.all() + if request.method == "POST": + form = PlantForm(request.POST, request.FILES, instance=plant) + if form.is_valid(): + form.save() + return redirect("plants:plant_detail_view", plant_id=plant.id) + else: + form = PlantForm(instance=plant) + return render(request, "plants/update_plant.html", {"form": form, "plant": plant, "countries": countries}) + + +def delete_plant_view(request, plant_id): + + if not request.user.is_authenticated or not request.user.is_staff: + messages.error(request, "Access Denied: You do not have permission to delete plant data.", "alert-danger") + return redirect("plants:plant_detail_view", plant_id=plant_id) + + plant = Plant.objects.get(id=plant_id) + plant.delete() + return redirect("plants:all_plants_view") + + +def search_view(request): + if "search" in request.GET and len(request.GET["search"]) >= 2: + results = Plant.objects.filter(name__icontains=request.GET["search"]) + else: + results = [] + + return render(request, "plants/search.html", {"results": results}) + + +def add_review_view(request: HttpRequest, plant_id): + + if not request.user.is_authenticated: + messages.warning(request, "You must be logged in to share your insights.", "alert-warning") + return redirect("accounts:sign_in") + + plant_object = Plant.objects.get(pk=plant_id) + + if request.method == "POST": + new_review = Review( + plant=plant_object, + user=request.user, + comment=request.POST["comment"] + ) + new_review.save() + + return redirect("plants:plant_detail_view", plant_id=plant_id) + + +def country_plants_view(request, country_id): + country = get_object_or_404(Country, id=country_id) + plants = Plant.objects.filter(countries=country) + + return render(request, 'plants/country_plants.html', { + 'country': country, + 'plants': plants + }) \ No newline at end of file