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..91cba2e --- /dev/null +++ b/Planteer/Planteer/settings.py @@ -0,0 +1,132 @@ +""" +Django settings for Planteer project. + +Generated by 'django-admin startproject' using Django 6.0.3. + +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-%0cc1#2i4e^(nfrm&@y5#6tcqx@lnftl89^+@cj*l(9l9l44vx' + +# 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', + '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': [BASE_DIR / 'templates'], + '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/' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = 'accounts:signin' +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' diff --git a/Planteer/Planteer/urls.py b/Planteer/Planteer/urls.py new file mode 100644 index 0000000..ea8b7ac --- /dev/null +++ b/Planteer/Planteer/urls.py @@ -0,0 +1,30 @@ +""" +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('accounts/', include('accounts.urls')), +] + +if settings.DEBUG: + urlpatterns += 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/forms.py b/Planteer/accounts/forms.py new file mode 100644 index 0000000..c9c4fef --- /dev/null +++ b/Planteer/accounts/forms.py @@ -0,0 +1,48 @@ +from django import forms +from django.contrib.auth.models import User + + +class SignUpForm(forms.Form): + username = forms.CharField( + widget=forms.TextInput(attrs={'placeholder': 'Username'}) + ) + email = forms.EmailField( + widget=forms.EmailInput(attrs={'placeholder': 'Email'}) + ) + password = forms.CharField( + widget=forms.PasswordInput(attrs={'placeholder': 'Password'}) + ) + confirm_password = forms.CharField( + widget=forms.PasswordInput(attrs={'placeholder': 'Confirm Password'}) + ) + + def clean_username(self): + username = self.cleaned_data.get('username', '').strip() + if User.objects.filter(username=username).exists(): + raise forms.ValidationError('This username is already taken.') + return username + + def clean_email(self): + email = self.cleaned_data.get('email', '').strip() + if User.objects.filter(email=email).exists(): + raise forms.ValidationError('This email is already registered.') + return email + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get('password') + confirm_password = cleaned_data.get('confirm_password') + + if password and confirm_password and password != confirm_password: + raise forms.ValidationError('Passwords do not match.') + + return cleaned_data + + +class SignInForm(forms.Form): + username = forms.CharField( + widget=forms.TextInput(attrs={'placeholder': 'Username'}) + ) + password = forms.CharField( + widget=forms.PasswordInput(attrs={'placeholder': 'Password'}) + ) 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/signin.html b/Planteer/accounts/templates/accounts/signin.html new file mode 100644 index 0000000..084f00b --- /dev/null +++ b/Planteer/accounts/templates/accounts/signin.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block title %}Sign In{% endblock %} + +{% block content %} +
+
+
+

Sign In

+

Welcome back

+ + {% if error_message %} +
+

{{ error_message }}

+
+ {% endif %} + +
+ {% csrf_token %} + +
+ + {{ form.username }} + {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {{ form.password }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ + +
+ + +
+
+
+{% endblock %} diff --git a/Planteer/accounts/templates/accounts/signup.html b/Planteer/accounts/templates/accounts/signup.html new file mode 100644 index 0000000..da6eac2 --- /dev/null +++ b/Planteer/accounts/templates/accounts/signup.html @@ -0,0 +1,62 @@ +{% extends 'base.html' %} + +{% block title %}Sign Up{% endblock %} + +{% block content %} +
+
+
+

Sign Up

+

Create a new account

+ + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} + +
+ + {{ form.username }} + {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {{ form.email }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {{ form.password }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {{ form.confirm_password }} + {% for error in form.confirm_password.errors %} + {{ error }} + {% endfor %} +
+ + +
+ + +
+
+
+{% endblock %} 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..f954140 --- /dev/null +++ b/Planteer/accounts/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = "accounts" + +urlpatterns = [ + path('signup/', views.sign_up, name='signup'), + path('signin/', views.sign_in, name='signin'), + path('logout/', views.log_out, name='logout'), +] diff --git a/Planteer/accounts/views.py b/Planteer/accounts/views.py new file mode 100644 index 0000000..167b858 --- /dev/null +++ b/Planteer/accounts/views.py @@ -0,0 +1,54 @@ +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.models import User +from django.shortcuts import render, redirect + +from .forms import SignUpForm, SignInForm + + +def sign_up(request): + if request.user.is_authenticated: + return redirect('main:home') + + if request.method == 'POST': + form = SignUpForm(request.POST) + if form.is_valid(): + user = User.objects.create_user( + username=form.cleaned_data['username'], + email=form.cleaned_data['email'], + password=form.cleaned_data['password'], + ) + login(request, user) + return redirect('main:home') + else: + form = SignUpForm() + + return render(request, 'accounts/signup.html', {'form': form}) + + +def sign_in(request): + if request.user.is_authenticated: + return redirect('main:home') + + form = SignInForm(request.POST or None) + error_message = None + + if request.method == 'POST' and form.is_valid(): + user = authenticate( + request, + username=form.cleaned_data['username'], + password=form.cleaned_data['password'], + ) + if user is not None: + login(request, user) + return redirect('main:home') + error_message = 'Invalid username or password' + + return render(request, 'accounts/signin.html', { + 'form': form, + 'error_message': error_message, + }) + + +def log_out(request): + logout(request) + return redirect('main:home') diff --git a/Planteer/db.sqlite3 b/Planteer/db.sqlite3 new file mode 100644 index 0000000..237702b 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..d407bfd --- /dev/null +++ b/Planteer/main/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import ContactMessage + + +@admin.register(ContactMessage) +class ContactMessageAdmin(admin.ModelAdmin): + list_display = ['first_name', 'last_name', 'email', 'created_at'] + list_filter = ['created_at'] + search_fields = ['first_name', 'last_name', 'email'] \ No newline at end of file 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/forms.py b/Planteer/main/forms.py new file mode 100644 index 0000000..75b991b --- /dev/null +++ b/Planteer/main/forms.py @@ -0,0 +1,27 @@ +from django import forms +from .models import ContactMessage + + +class ContactForm(forms.ModelForm): + + class Meta: + model = ContactMessage + fields = ['first_name', 'last_name', 'email', 'message'] + widgets = { + 'first_name': forms.TextInput(attrs={'placeholder': 'Jane', 'required': True}), + 'last_name': forms.TextInput(attrs={'placeholder': 'Smitherton', 'required': True}), + 'email': forms.EmailInput(attrs={'placeholder': 'email@fakedomain.net', 'required': True}), + 'message': forms.Textarea(attrs={'placeholder': 'Enter your question or message', 'rows': 5, 'required': True}), + } + + def clean_first_name(self): + name = self.cleaned_data.get('first_name', '').strip() + if len(name) < 2: + raise forms.ValidationError("First name must be at least 2 characters.") + return name + + def clean_message(self): + msg = self.cleaned_data.get('message', '').strip() + if len(msg) < 10: + raise forms.ValidationError("Message must be at least 10 characters.") + return msg \ No newline at end of file diff --git a/Planteer/main/migrations/0001_initial.py b/Planteer/main/migrations/0001_initial.py new file mode 100644 index 0000000..b96653a --- /dev/null +++ b/Planteer/main/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.3 on 2026-04-17 23:45 + +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')), + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('subject', models.CharField(max_length=200)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_read', models.BooleanField(default=False)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/Planteer/main/migrations/0002_remove_contactmessage_is_read_and_more.py b/Planteer/main/migrations/0002_remove_contactmessage_is_read_and_more.py new file mode 100644 index 0000000..8e927a0 --- /dev/null +++ b/Planteer/main/migrations/0002_remove_contactmessage_is_read_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 6.0.4 on 2026-04-19 16:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='contactmessage', + name='is_read', + ), + migrations.RemoveField( + model_name='contactmessage', + name='name', + ), + migrations.RemoveField( + model_name='contactmessage', + name='subject', + ), + migrations.AddField( + model_name='contactmessage', + name='first_name', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='contactmessage', + name='last_name', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] 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..e64730e --- /dev/null +++ b/Planteer/main/models.py @@ -0,0 +1,15 @@ +from django.db import models + + +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) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"{self.first_name} {self.last_name} - {self.email}" \ No newline at end of file diff --git a/Planteer/main/templates/main/contact.html b/Planteer/main/templates/main/contact.html new file mode 100644 index 0000000..70dcdb3 --- /dev/null +++ b/Planteer/main/templates/main/contact.html @@ -0,0 +1,59 @@ +{% extends 'base.html' %} + +{% block title %}Contact Us{% endblock %} + +{% block content %} +
+
+

Contact us

+

Subheading for description or instructions

+ + {% if success %} +
Your message has been sent successfully!
+ {% endif %} + +
+
+
+ {% csrf_token %} + +
+
+ + {{ form.first_name }} + {% for error in form.first_name.errors %} + {{ error }} + {% endfor %} +
+
+ + {{ form.last_name }} + {% for error in form.last_name.errors %} + {{ error }} + {% endfor %} +
+
+ +
+ + {{ form.email }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {{ form.message }} + {% for error in form.message.errors %} + {{ error }} + {% endfor %} +
+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/main/templates/main/contact_messages.html b/Planteer/main/templates/main/contact_messages.html new file mode 100644 index 0000000..0934f6e --- /dev/null +++ b/Planteer/main/templates/main/contact_messages.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Messages{% endblock %} + +{% block content %} +
+
+

Messages from Users

+ + {% if messages_list %} +
+ {% for msg in messages_list %} +
+
+

{{ msg.first_name }} {{ msg.last_name }}

+ {{ msg.email }} +

{{ msg.message }}

+
+ {% endfor %} +
+ {% else %} +
+

No messages yet

+

When someone contacts you, their messages will appear here.

+
+ {% endif %} +
+
+{% 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..2020f98 --- /dev/null +++ b/Planteer/main/templates/main/home.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Home{% endblock %} + +{% block content %} + +
+
+

Planteer

+

Plant Database For Plants Lovers

+ +
+
+ + +
+
+
+
+

Plants

+

Learn more about plants

+
+ More → +
+ + {% if featured_plants %} + + + {% else %} +

No plants yet. Add one!

+ {% endif %} +
+
+{% 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..2a6a03c --- /dev/null +++ b/Planteer/main/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = "main" + +urlpatterns = [ + path('', views.home, name='home'), + path('contact/', views.contact, name='contact'), + path('contact/messages/', views.contact_messages, name='contact_messages'), +] \ No newline at end of file diff --git a/Planteer/main/views.py b/Planteer/main/views.py new file mode 100644 index 0000000..255203e --- /dev/null +++ b/Planteer/main/views.py @@ -0,0 +1,38 @@ +from django.shortcuts import render, redirect +from django.http import HttpRequest, HttpResponse +from plants.models import Plant, Category +from .forms import ContactForm +from .models import ContactMessage + + +def home(request: HttpRequest): + featured_plants = Plant.objects.all()[:6] + categories = Category.objects.all() + + return render(request, 'main/home.html', { + 'featured_plants': featured_plants, + 'categories': categories, + }) + + +def contact(request: HttpRequest): + if request.method == 'POST': + form = ContactForm(request.POST) + if form.is_valid(): + form.save() + return render(request, 'main/contact.html', { + 'form': ContactForm(), + 'success': True, + }) + else: + form = ContactForm() + + return render(request, 'main/contact.html', {'form': form}) + + +def contact_messages(request: HttpRequest): + messages_list = ContactMessage.objects.all() + + return render(request, 'main/contact_messages.html', { + 'messages_list': messages_list, + }) \ 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/Egypt.htm b/Planteer/media/flags/Egypt.htm new file mode 100644 index 0000000..8935738 --- /dev/null +++ b/Planteer/media/flags/Egypt.htm @@ -0,0 +1,3043 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Flag of Egypt | History, Colors, Symbols | Britannica + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + +
+ + + +
+ + + +
+ + + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+
+ + + +
+ + flag of Egypt + + + +
+
+
+ + +
+ + + + + + +
+ + +
+ +
+ +
+ +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+ + Britannica AI Icon + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ verifiedCite +
+
+ While every effort has been made to follow citation style rules, there may be some discrepancies. + Please refer to the appropriate style manual or other sources if you have any questions. +
+
Select Citation Style
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ Feedback +
+ +
+ +
+ Corrections? Updates? Omissions? Let us know if you have suggestions to improve this article (requires login). +
+ +
+ + +
+ + + + + +
+ +
+
Thank you for your feedback
+

Our editors will review what you’ve submitted and determine whether to revise the article.

+
+
+
+ +
+
+
+ External Websites +
+ + +
Britannica Websites
+
Articles from Britannica Encyclopedias for elementary and high school students.
+ + + + +
+
+
+ +
+
+ + + + + + + + + + + +
+
+ +
+ +
+
+ +
+

flag of Egypt

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

Many flags have been flown over Egypt in its thousands of years of history, but its first true national flag was established only on February 16, 1915, after the British, who had effectively controlled the country since 1882, formally proclaimed a protectorate to deter restoration of Egypt’s nominal ties to the Ottoman Empire. The flag previously used by the khedive (the Ottoman viceroy in Egypt) became the national flag; it was red with three white crescents and stars. Participants in the revolt of 1919 hoisted a green flag with a white crescent and cross, indicating unity between Muslims and Christians in the struggle for independence. A similar flag with three white stars instead of the cross was adopted on December 10, 1923, following the proclamation of the Kingdom of Egypt.

The 1952 revolt established the Arab Liberation Flag, which had red-white-black horizontal stripes and a gold eagle. That flag was often flown beside the national flag but did not itself have official status; nevertheless, its design was reflected in the official 1958 national flag of the United Arab Republic, where the gold eagle was replaced by two green stars to symbolize the union of Egypt and Syria. It was anticipated that the number of stars would increase as other Arab states joined the union. In fact, Syria seceded from the union, although Egypt did not alter the flag to reflect this. On January 1, 1972, the Confederation of Arab Republics was established between Egypt, Syria, and Libya. The stars were replaced with the gold hawk of Quraysh, symbol of the tribe to which the Prophet Muhammad had belonged. Finally, on October 9, 1984, five years after the dissolution of the federation, the gold eagle of Saladin—12th-century ruler of Egypt, Syria, Yemen, and Palestine—was substituted for the hawk.

+ + + +
+ +
+ +
+ +
+ + +
+
+
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/Planteer/media/flags/ronaldo.jpeg b/Planteer/media/flags/ronaldo.jpeg new file mode 100644 index 0000000..500a5b8 Binary files /dev/null and b/Planteer/media/flags/ronaldo.jpeg differ diff --git "a/Planteer/media/flags/\330\247\331\204\330\247\330\261\330\257\331\206.avif" "b/Planteer/media/flags/\330\247\331\204\330\247\330\261\330\257\331\206.avif" new file mode 100644 index 0000000..3b4ea8d Binary files /dev/null and "b/Planteer/media/flags/\330\247\331\204\330\247\330\261\330\257\331\206.avif" differ diff --git "a/Planteer/media/flags/\330\247\331\204\330\263\330\271\331\210\330\257\331\212\330\251.jpg" "b/Planteer/media/flags/\330\247\331\204\330\263\330\271\331\210\330\257\331\212\330\251.jpg" new file mode 100644 index 0000000..02223f8 Binary files /dev/null and "b/Planteer/media/flags/\330\247\331\204\330\263\330\271\331\210\330\257\331\212\330\251.jpg" differ diff --git "a/Planteer/media/flags/\330\247\331\204\330\271\330\261\330\247\331\202.webp" "b/Planteer/media/flags/\330\247\331\204\330\271\330\261\330\247\331\202.webp" new file mode 100644 index 0000000..4ccf4cf Binary files /dev/null and "b/Planteer/media/flags/\330\247\331\204\330\271\330\261\330\247\331\202.webp" differ diff --git "a/Planteer/media/flags/\330\271\331\205\330\247\331\206.png" "b/Planteer/media/flags/\330\271\331\205\330\247\331\206.png" new file mode 100644 index 0000000..77f3fa6 Binary files /dev/null and "b/Planteer/media/flags/\330\271\331\205\330\247\331\206.png" differ diff --git "a/Planteer/media/flags/\331\205\330\265\330\261.webp" "b/Planteer/media/flags/\331\205\330\265\330\261.webp" new file mode 100644 index 0000000..df6cd9f Binary files /dev/null and "b/Planteer/media/flags/\331\205\330\265\330\261.webp" differ diff --git "a/Planteer/media/flags/\331\205\330\272\330\261\330\250.jpg" "b/Planteer/media/flags/\331\205\330\272\330\261\330\250.jpg" new file mode 100644 index 0000000..16f93cf Binary files /dev/null and "b/Planteer/media/flags/\331\205\330\272\330\261\330\250.jpg" differ diff --git "a/Planteer/media/plants/\330\247\331\203\331\204\331\212\331\204_\330\247\331\204\330\254\330\250\331\204.webp" "b/Planteer/media/plants/\330\247\331\203\331\204\331\212\331\204_\330\247\331\204\330\254\330\250\331\204.webp" new file mode 100644 index 0000000..0a1e90b Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\203\331\204\331\212\331\204_\330\247\331\204\330\254\330\250\331\204.webp" differ diff --git "a/Planteer/media/plants/\330\247\331\204\330\250\331\202\330\257\331\210\331\206\330\263.jpg" "b/Planteer/media/plants/\330\247\331\204\330\250\331\202\330\257\331\210\331\206\330\263.jpg" new file mode 100644 index 0000000..8f29c27 Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\330\250\331\202\330\257\331\210\331\206\330\263.jpg" differ diff --git "a/Planteer/media/plants/\330\247\331\204\330\250\331\202\331\204\330\251.webp" "b/Planteer/media/plants/\330\247\331\204\330\250\331\202\331\204\330\251.webp" new file mode 100644 index 0000000..19bdccd Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\330\250\331\202\331\204\330\251.webp" differ diff --git "a/Planteer/media/plants/\330\247\331\204\330\256\330\250\331\212\330\262\330\251.jpg" "b/Planteer/media/plants/\330\247\331\204\330\256\330\250\331\212\330\262\330\251.jpg" new file mode 100644 index 0000000..ba3692c Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\330\256\330\250\331\212\330\262\330\251.jpg" differ diff --git "a/Planteer/media/plants/\330\247\331\204\330\256\330\261\331\210\330\271.jpg" "b/Planteer/media/plants/\330\247\331\204\330\256\330\261\331\210\330\271.jpg" new file mode 100644 index 0000000..99595b2 Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\330\256\330\261\331\210\330\271.jpg" differ diff --git "a/Planteer/media/plants/\330\247\331\204\330\264\331\210\331\203\330\261\330\247\331\206.jpg" "b/Planteer/media/plants/\330\247\331\204\330\264\331\210\331\203\330\261\330\247\331\206.jpg" new file mode 100644 index 0000000..9236a27 Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\330\264\331\210\331\203\330\261\330\247\331\206.jpg" differ diff --git "a/Planteer/media/plants/\330\247\331\204\330\271\331\206\330\265\331\204.jpg" "b/Planteer/media/plants/\330\247\331\204\330\271\331\206\330\265\331\204.jpg" new file mode 100644 index 0000000..20965c7 Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\330\271\331\206\330\265\331\204.jpg" differ diff --git "a/Planteer/media/plants/\330\247\331\204\331\207\331\206\330\257\330\250\330\247\330\241.webp" "b/Planteer/media/plants/\330\247\331\204\331\207\331\206\330\257\330\250\330\247\330\241.webp" new file mode 100644 index 0000000..bf02764 Binary files /dev/null and "b/Planteer/media/plants/\330\247\331\204\331\207\331\206\330\257\330\250\330\247\330\241.webp" differ diff --git "a/Planteer/media/plants/\330\252\331\201\330\247\330\255_\330\247\331\204\330\264\331\212\330\267\330\247\331\206.jpg" "b/Planteer/media/plants/\330\252\331\201\330\247\330\255_\330\247\331\204\330\264\331\212\330\267\330\247\331\206.jpg" new file mode 100644 index 0000000..f4edcd9 Binary files /dev/null and "b/Planteer/media/plants/\330\252\331\201\330\247\330\255_\330\247\331\204\330\264\331\212\330\267\330\247\331\206.jpg" differ diff --git "a/Planteer/media/plants/\330\261\331\212\330\255\330\247\331\206.jpg" "b/Planteer/media/plants/\330\261\331\212\330\255\330\247\331\206.jpg" new file mode 100644 index 0000000..308e3da Binary files /dev/null and "b/Planteer/media/plants/\330\261\331\212\330\255\330\247\331\206.jpg" differ diff --git "a/Planteer/media/plants/\330\262\331\206\330\250\331\202_\330\247\331\204\331\210\330\247\330\257\331\212.jpg" "b/Planteer/media/plants/\330\262\331\206\330\250\331\202_\330\247\331\204\331\210\330\247\330\257\331\212.jpg" new file mode 100644 index 0000000..8d8c5b8 Binary files /dev/null and "b/Planteer/media/plants/\330\262\331\206\330\250\331\202_\330\247\331\204\331\210\330\247\330\257\331\212.jpg" differ diff --git "a/Planteer/media/plants/\330\263\330\252_\330\247\331\204\330\255\330\263\331\206.jpg" "b/Planteer/media/plants/\330\263\330\252_\330\247\331\204\330\255\330\263\331\206.jpg" new file mode 100644 index 0000000..fae4f6f Binary files /dev/null and "b/Planteer/media/plants/\330\263\330\252_\330\247\331\204\330\255\330\263\331\206.jpg" differ diff --git "a/Planteer/media/plants/\331\202\331\201\330\247\330\262_\330\247\331\204\330\253\330\271\331\204\330\250.webp" "b/Planteer/media/plants/\331\202\331\201\330\247\330\262_\330\247\331\204\330\253\330\271\331\204\330\250.webp" new file mode 100644 index 0000000..01e6b0e Binary files /dev/null and "b/Planteer/media/plants/\331\202\331\201\330\247\330\262_\330\247\331\204\330\253\330\271\331\204\330\250.webp" differ diff --git "a/Planteer/media/plants/\331\206\330\271\331\206\330\247\330\271.jpg" "b/Planteer/media/plants/\331\206\330\271\331\206\330\247\330\271.jpg" new file mode 100644 index 0000000..5eeada4 Binary files /dev/null and "b/Planteer/media/plants/\331\206\330\271\331\206\330\247\330\271.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..6530482 --- /dev/null +++ b/Planteer/plants/admin.py @@ -0,0 +1,29 @@ +from django.contrib import admin +from .models import Category, Plant, Country, Comment + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ['name', 'description'] + search_fields = ['name'] + + +@admin.register(Plant) +class PlantAdmin(admin.ModelAdmin): + list_display = ['name', 'category', 'is_edible', 'created_at'] + list_filter = ['category', 'is_edible'] + search_fields = ['name', 'scientific_name'] + filter_horizontal = ['countries'] + + +@admin.register(Country) +class CountryAdmin(admin.ModelAdmin): + list_display = ['name'] + search_fields = ['name'] + + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ['user', 'plant', 'created_at'] + list_filter = ['created_at'] + search_fields = ['user__username', 'body'] \ No newline at end of file diff --git a/Planteer/plants/apps.py b/Planteer/plants/apps.py new file mode 100644 index 0000000..2dee018 --- /dev/null +++ b/Planteer/plants/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PlantConfig(AppConfig): + name = 'plants' diff --git a/Planteer/plants/forms.py b/Planteer/plants/forms.py new file mode 100644 index 0000000..74efea5 --- /dev/null +++ b/Planteer/plants/forms.py @@ -0,0 +1,65 @@ +from django import forms +from .models import Plant, Category, Country, Comment + + +class PlantForm(forms.ModelForm): + + class Meta: + model = Plant + fields = ['name', 'category', 'description', 'image', 'is_edible', 'countries'] + widgets = { + 'name': forms.TextInput(attrs={'placeholder': 'Plant name'}), + 'category': forms.Select(), + 'description': forms.Textarea(attrs={'placeholder': 'Description', 'rows': 4}), + 'image': forms.ClearableFileInput(attrs={'accept': 'image/*'}), + 'is_edible': forms.CheckboxInput(), + 'countries': forms.CheckboxSelectMultiple(), + } + + def clean_name(self): + name = self.cleaned_data.get('name', '').strip() + if len(name) < 2: + raise forms.ValidationError("Plant name must be at least 2 characters.") + return name + + def clean_description(self): + desc = self.cleaned_data.get('description', '').strip() + if not desc: + raise forms.ValidationError("Description is required.") + return desc + + +class PlantFilterForm(forms.Form): + name = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': 'Search by plant name...'}), + ) + is_edible = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=[('', 'All Plants'), ('true', 'Edible'), ('false', 'Non-Edible')], + ) + ) + country = forms.ModelChoiceField( + queryset=Country.objects.all(), + required=False, + empty_label='All Countries', + widget=forms.Select(), + ) + + +class CommentForm(forms.ModelForm): + + class Meta: + model = Comment + fields = ['body'] + labels = { + 'body': 'Comment', + } + widgets = { + 'body': forms.Textarea(attrs={ + 'placeholder': 'Write your comment...', + 'rows': 3, + 'required': True, + }), + } \ 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..d1575d0 --- /dev/null +++ b/Planteer/plants/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 6.0.3 on 2026-04-17 23:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.TextField(blank=True)), + ('icon', models.CharField(default='🌿', max_length=10)), + ], + options={ + 'verbose_name_plural': 'Categories', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Plant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('scientific_name', models.CharField(blank=True, max_length=200)), + ('description', models.TextField()), + ('image', models.ImageField(blank=True, null=True, upload_to='plants/')), + ('image_url', models.URLField(blank=True, help_text='رابط صورة خارجي (بديل عن رفع صورة)')), + ('is_edible', models.BooleanField(default=False)), + ('sunlight', models.CharField(choices=[('full_sun', 'Full Sun ☀️'), ('partial', 'Partial Shade ⛅'), ('shade', 'Full Shade 🌥️')], default='full_sun', max_length=20)), + ('water_needs', models.CharField(choices=[('low', 'Low 💧'), ('medium', 'Medium 💧💧'), ('high', 'High 💧💧💧')], default='medium', max_length=20)), + ('difficulty', models.CharField(choices=[('easy', 'Easy 🟢'), ('medium', 'Medium 🟡'), ('hard', 'Hard 🔴')], default='easy', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plants', to='plants.category')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/Planteer/plants/migrations/0002_remove_category_icon_remove_plant_image_url_and_more.py b/Planteer/plants/migrations/0002_remove_category_icon_remove_plant_image_url_and_more.py new file mode 100644 index 0000000..157d77f --- /dev/null +++ b/Planteer/plants/migrations/0002_remove_category_icon_remove_plant_image_url_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.4 on 2026-04-19 16:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='category', + name='icon', + ), + migrations.RemoveField( + model_name='plant', + name='image_url', + ), + migrations.AlterField( + model_name='plant', + name='difficulty', + field=models.CharField(choices=[('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard')], default='easy', max_length=20), + ), + migrations.AlterField( + model_name='plant', + name='sunlight', + field=models.CharField(choices=[('full_sun', 'Full Sun'), ('partial', 'Partial Shade'), ('shade', 'Full Shade')], default='full_sun', max_length=20), + ), + migrations.AlterField( + model_name='plant', + name='water_needs', + field=models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=20), + ), + ] diff --git a/Planteer/plants/migrations/0003_alter_plant_category.py b/Planteer/plants/migrations/0003_alter_plant_category.py new file mode 100644 index 0000000..f6073eb --- /dev/null +++ b/Planteer/plants/migrations/0003_alter_plant_category.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.4 on 2026-04-19 19:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0002_remove_category_icon_remove_plant_image_url_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='plant', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plants', to='plants.category'), + ), + ] diff --git a/Planteer/plants/migrations/0004_remove_plant_difficulty_remove_plant_sunlight_and_more.py b/Planteer/plants/migrations/0004_remove_plant_difficulty_remove_plant_sunlight_and_more.py new file mode 100644 index 0000000..b18cd4d --- /dev/null +++ b/Planteer/plants/migrations/0004_remove_plant_difficulty_remove_plant_sunlight_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.4 on 2026-04-19 19:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0003_alter_plant_category'), + ] + + operations = [ + migrations.RemoveField( + model_name='plant', + name='difficulty', + ), + migrations.RemoveField( + model_name='plant', + name='sunlight', + ), + migrations.RemoveField( + model_name='plant', + name='water_needs', + ), + ] diff --git a/Planteer/plants/migrations/0005_review.py b/Planteer/plants/migrations/0005_review.py new file mode 100644 index 0000000..651204f --- /dev/null +++ b/Planteer/plants/migrations/0005_review.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.4 on 2026-04-20 06:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0004_remove_plant_difficulty_remove_plant_sunlight_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rating', models.PositiveIntegerField()), + ('comment', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('plant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='plants.plant')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/Planteer/plants/migrations/0006_alter_review_options_remove_review_created_at_and_more.py b/Planteer/plants/migrations/0006_alter_review_options_remove_review_created_at_and_more.py new file mode 100644 index 0000000..241a448 --- /dev/null +++ b/Planteer/plants/migrations/0006_alter_review_options_remove_review_created_at_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.4 on 2026-04-21 05:50 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0005_review'), + ] + + operations = [ + migrations.AlterModelOptions( + name='review', + options={'ordering': ['-created']}, + ), + migrations.RemoveField( + model_name='review', + name='created_at', + ), + migrations.AddField( + model_name='review', + name='created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/Planteer/plants/migrations/0007_country_plant_countries.py b/Planteer/plants/migrations/0007_country_plant_countries.py new file mode 100644 index 0000000..4a7ad09 --- /dev/null +++ b/Planteer/plants/migrations/0007_country_plant_countries.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.4 on 2026-04-21 19:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0006_alter_review_options_remove_review_created_at_and_more'), + ] + + 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, unique=True)), + ('flag', models.ImageField(blank=True, null=True, upload_to='flags/')), + ], + options={ + 'verbose_name_plural': 'Countries', + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='plant', + name='countries', + field=models.ManyToManyField(blank=True, related_name='plants', to='plants.country'), + ), + ] diff --git a/Planteer/plants/migrations/0008_comment.py b/Planteer/plants/migrations/0008_comment.py new file mode 100644 index 0000000..b539167 --- /dev/null +++ b/Planteer/plants/migrations/0008_comment.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.4 on 2026-04-26 16:44 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0007_country_plant_countries'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('plant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='plants.plant')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/Planteer/plants/migrations/0009_delete_review.py b/Planteer/plants/migrations/0009_delete_review.py new file mode 100644 index 0000000..dadad9e --- /dev/null +++ b/Planteer/plants/migrations/0009_delete_review.py @@ -0,0 +1,16 @@ +# Generated by Django 6.0.4 on 2026-04-26 16:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0008_comment'), + ] + + operations = [ + migrations.DeleteModel( + name='Review', + ), + ] 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..f4ab59b --- /dev/null +++ b/Planteer/plants/models.py @@ -0,0 +1,70 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Category(models.Model): + name = models.CharField(max_length=100, unique=True) + description = models.TextField(blank=True) + + class Meta: + verbose_name_plural = "Categories" + ordering = ['name'] + + def __str__(self): + return self.name + + +class Country(models.Model): + name = models.CharField(max_length=100, unique=True) + flag = models.ImageField(upload_to='flags/', blank=True, null=True) + + class Meta: + verbose_name_plural = "Countries" + ordering = ['name'] + + def __str__(self): + return self.name + + def get_flag(self): + if self.flag: + return self.flag.url + return '/static/images/default-flag.svg' + + +class Plant(models.Model): + name = models.CharField(max_length=200) + scientific_name = models.CharField(max_length=200, blank=True) + description = models.TextField() + category = models.ForeignKey(Category, on_delete=models.SET_NULL, related_name='plants', null=True, blank=True) + image = models.ImageField(upload_to='plants/', blank=True, null=True) + is_edible = models.BooleanField(default=False) + countries = models.ManyToManyField(Country, related_name='plants', blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return self.name + + def get_image(self): + if self.image: + return self.image.url + return '/static/images/default-plant.png' + + def get_related_plants(self): + return Plant.objects.filter(category=self.category).exclude(pk=self.pk)[:4] + + +class Comment(models.Model): + plant = models.ForeignKey(Plant, on_delete=models.CASCADE, related_name='comments') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments') + body = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"{self.user.username} on {self.plant.name}" diff --git a/Planteer/plants/templates/plants/all_plants.html b/Planteer/plants/templates/plants/all_plants.html new file mode 100644 index 0000000..a0cadd9 --- /dev/null +++ b/Planteer/plants/templates/plants/all_plants.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} + +{% block title %}All Plants{% endblock %} + +{% block content %} +
+
+

All Plants

+ + +
+
+ + {{ filter_form.name }} +
+
+ + {{ filter_form.is_edible }} +
+
+ + {{ filter_form.country }} +
+ + Clear +
+ + {% if plants %} +
+ {% for plant in plants %} + {% include 'plants/includes/plant_card.html' %} + {% endfor %} +
+ {% else %} +
+

No plants found

+

Try changing your filters or add a new plant.

+
+ {% endif %} +
+
+{% 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..514f2fc --- /dev/null +++ b/Planteer/plants/templates/plants/includes/plant_card.html @@ -0,0 +1,22 @@ +{% load static %} + + +
+ {{ plant.name }} +
+
+

{{ plant.name }}

+

{{ plant.description|truncatewords:8 }}

+

{{ plant.category.name }}

+ {% if plant.countries.all %} +
+ {% for c in plant.countries.all %} + + {{ c.name }} + {{ c.name }} + + {% endfor %} +
+ {% endif %} +
+
diff --git a/Planteer/plants/templates/plants/plant_delete.html b/Planteer/plants/templates/plants/plant_delete.html new file mode 100644 index 0000000..dd8fe10 --- /dev/null +++ b/Planteer/plants/templates/plants/plant_delete.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Delete {{ plant.name }}{% endblock %} + +{% block content %} +
+
+
+

Delete Plant

+

Are you sure you want to delete "{{ plant.name }}"?

+

This action cannot be undone.

+ +
+ {{ plant.name }} +
+

{{ plant.name }}

+

{{ plant.category.name }}

+
+
+ +
+ {% csrf_token %} + + Cancel +
+
+
+
+{% endblock %} \ 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..7a32483 --- /dev/null +++ b/Planteer/plants/templates/plants/plant_detail.html @@ -0,0 +1,98 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}{{ plant.name }}{% endblock %} + +{% block content %} +
+
+
+
+ {{ plant.name }} +
+
+

{{ plant.name }}

+

{{ plant.category.name }}

+

{{ plant.description }}

+ +
+

Is Edible: {% if plant.is_edible %}Yes{% else %}No{% endif %}

+ {% if plant.scientific_name %} +

Scientific: {{ plant.scientific_name }}

+ {% endif %} +
+ +
+

Native to:

+
+ {% for country in plant.countries.all %} + + {{ country.name }} + {{ country.name }} + + {% empty %} + No countries added for this plant yet. + {% endfor %} +
+
+ +
+ Edit + Delete +
+ +

Comments ({{ comments|length }})

+ + {% if user.is_authenticated %} +
+

Commenting as {{ user.username }}

+
+ {% csrf_token %} +
+ + {{ comment_form.body }} + {% for error in comment_form.body.errors %} + {{ error }} + {% endfor %} +
+ +
+
+ {% else %} + + {% endif %} + +
+ {% for comment in comments %} +
+
+ {{ comment.user.username }} + {{ comment.created_at|date:"N j, Y, g:i a" }} +
+

{{ comment.body }}

+
+ {% empty %} +

No comments yet. Be the first to comment!

+ {% endfor %} +
+
+
+
+
+ + +{% if related_plants %} +
+
+

Related Plants

+
+ {% for rp in related_plants %} + {% include 'plants/includes/plant_card.html' with plant=rp %} + {% endfor %} +
+
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/plant_form.html b/Planteer/plants/templates/plants/plant_form.html new file mode 100644 index 0000000..f86f976 --- /dev/null +++ b/Planteer/plants/templates/plants/plant_form.html @@ -0,0 +1,97 @@ +{% extends 'base.html' %} + +{% block title %}{{ action }} Plant{% endblock %} + +{% block content %} +
+
+

{{ action }} Plant

+ +
+
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +

{{ error }}

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

{{ field.label }}: {{ error }}

+ {% endfor %} + {% endfor %} +
+ {% endif %} + + +
+ + {{ form.name }} + {% for error in form.name.errors %} + {{ error }} + {% endfor %} +
+ + +
+ + {{ form.category }} + {% for error in form.category.errors %} + {{ error }} + {% endfor %} +
+ + +
+ + {{ form.description }} + {% for error in form.description.errors %} + {{ error }} + {% endfor %} +
+ + +
+ {{ form.is_edible }} + +
+ + +
+ +
+ {% for checkbox in form.countries %} + + {% empty %} + No countries found. Add countries from admin first. + {% endfor %} +
+ {% for error in form.countries.errors %} + {{ error }} + {% endfor %} +
+ + +
+ + +
{{ form.image }}
+
+ +
+ + Cancel +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/plants_by_country.html b/Planteer/plants/templates/plants/plants_by_country.html new file mode 100644 index 0000000..d7add2d --- /dev/null +++ b/Planteer/plants/templates/plants/plants_by_country.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block title %}Plants from {{ country.name }}{% endblock %} + +{% block content %} +
+
+
+ {% if country.get_flag %} + {{ country.name }} + {% endif %} +
+

{{ country.name }}

+

{{ total }} plant{{ total|pluralize }} native to this country

+
+
+ + {% if plants %} +
+ {% for plant in plants %} + {% include 'plants/includes/plant_card.html' %} + {% endfor %} +
+ {% else %} +
+

No plants found

+

No plants have been tagged with this country yet.

+
+ {% endif %} +
+
+{% endblock %} diff --git a/Planteer/plants/templates/plants/search.html b/Planteer/plants/templates/plants/search.html new file mode 100644 index 0000000..77273a8 --- /dev/null +++ b/Planteer/plants/templates/plants/search.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block title %}Search Results{% endblock %} + +{% block content %} +
+
+

Search Results

+ + + + {% if searched %} + {% if plants %} +
+ {% for plant in plants %} + {% include 'plants/includes/plant_card.html' %} + {% endfor %} +
+ {% else %} +
+

No plants found

+

Try a different search term.

+
+ {% endif %} + {% endif %} +
+
+{% 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..4059a02 --- /dev/null +++ b/Planteer/plants/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from . import views + +app_name = "plants" + +urlpatterns = [ + path('all/', views.all_plants, name='all_plants'), + path('search/', views.search_plants, name='search'), + path('new/', views.add_plant, name='add_plant'), + path('/detail/', views.plant_detail, name='plant_detail'), + path('/update/', views.update_plant, name='update_plant'), + path('/delete/', views.delete_plant, name='delete_plant'), + path('country//', views.plants_by_country, name='plants_by_country'), +] \ No newline at end of file diff --git a/Planteer/plants/views.py b/Planteer/plants/views.py new file mode 100644 index 0000000..aa454d0 --- /dev/null +++ b/Planteer/plants/views.py @@ -0,0 +1,123 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.http import HttpRequest +from django.contrib import messages +from .models import Plant, Category, Country, Comment +from .forms import PlantForm, PlantFilterForm, CommentForm + + +def all_plants(request: HttpRequest): + plants = Plant.objects.all() + filter_form = PlantFilterForm(request.GET) + + if filter_form.is_valid(): + name = filter_form.cleaned_data.get('name') + is_edible = filter_form.cleaned_data.get('is_edible') + country = filter_form.cleaned_data.get('country') + if name: + plants = plants.filter(name__icontains=name) + if is_edible is not None: + plants = plants.filter(is_edible=is_edible) + if country: + plants = plants.filter(countries=country) + + return render(request, 'plants/all_plants.html', { + 'plants': plants, + 'filter_form': filter_form, + 'total': plants.count(), + }) + + +def plant_detail(request: HttpRequest, plant_id): + plant = get_object_or_404(Plant, pk=plant_id) + comments = plant.comments.all() + comment_form = CommentForm() + + if request.method == 'POST' and request.user.is_authenticated: + comment_form = CommentForm(request.POST) + if comment_form.is_valid(): + comment = comment_form.save(commit=False) + comment.plant = plant + comment.user = request.user + comment.save() + return redirect('plants:plant_detail', plant_id=plant.pk) + + related_plants = plant.get_related_plants() + + return render(request, 'plants/plant_detail.html', { + 'plant': plant, + 'related_plants': related_plants, + 'comments': comments, + 'comment_form': comment_form, + }) + + +def add_plant(request: HttpRequest): + if request.method == 'POST': + form = PlantForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request, 'Plant added successfully!') + return redirect('plants:all_plants') + else: + form = PlantForm() + + return render(request, 'plants/plant_form.html', { + 'form': form, + 'action': 'Add', + }) + + +def update_plant(request: HttpRequest, plant_id): + plant = get_object_or_404(Plant, pk=plant_id) + + if request.method == 'POST': + form = PlantForm(request.POST, request.FILES, instance=plant) + if form.is_valid(): + form.save() + messages.success(request, 'Plant updated successfully!') + return redirect('plants:plant_detail', plant_id=plant.pk) + else: + form = PlantForm(instance=plant) + + return render(request, 'plants/plant_form.html', { + 'form': form, + 'action': 'Update', + 'plant': plant, + }) + + +def delete_plant(request: HttpRequest, plant_id): + plant = get_object_or_404(Plant, pk=plant_id) + + if request.method == 'POST': + plant.delete() + return redirect('plants:all_plants') + + return render(request, 'plants/plant_delete.html', {'plant': plant}) + + +def search_plants(request: HttpRequest): + plants = Plant.objects.none() + query = request.GET.get('q', '').strip() + searched = bool(request.GET) + + if query: + plants = Plant.objects.filter(name__icontains=query) | Plant.objects.filter(description__icontains=query) + + return render(request, 'plants/search.html', { + 'plants': plants, + 'query': query, + 'searched': searched, + 'result_count': plants.count(), + }) + + +def plants_by_country(request: HttpRequest, country_id): + country = get_object_or_404(Country, pk=country_id) + plants = country.plants.all() + + return render(request, 'plants/plants_by_country.html', { + 'country': country, + 'plants': plants, + 'total': plants.count(), + }) \ No newline at end of file diff --git a/Planteer/static/css/style.css b/Planteer/static/css/style.css new file mode 100644 index 0000000..f7757d5 --- /dev/null +++ b/Planteer/static/css/style.css @@ -0,0 +1,1240 @@ +/* ============================================ + PLANTEER - Clean Minimal Design + Matching wireframe: black, white, simple + ============================================ */ + +/* ===== RESET ===== */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { scroll-behavior: smooth; } + +body { + font-family: 'Inter', system-ui, sans-serif; + color: #1a1a1a; + background: #fff; + line-height: 1.6; +} + +a { text-decoration: none; color: inherit; } +img { max-width: 100%; height: auto; display: block; } +button { cursor: pointer; font-family: inherit; } + +.container { + max-width: 1140px; + margin: 0 auto; + padding: 0 24px; +} + +/* ===== NAVBAR ===== */ +.navbar { + position: sticky; + top: 0; + z-index: 100; + background: rgba(255, 255, 255, 0.9); + border-bottom: 1px solid #e7e7e7; + backdrop-filter: blur(8px); +} + +.navbar::after { + content: ""; + display: block; + height: 2px; + background: linear-gradient(90deg, #1a1a1a 0%, #4f4f4f 50%, #1a1a1a 100%); + opacity: 0.14; +} + +.navbar-inner { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 70px; + gap: 20px; +} + +.navbar-logo { + font-family: 'Playfair Display', serif; + font-size: 1.35rem; + font-weight: 700; + color: #1a1a1a; + letter-spacing: 0.2px; + flex-shrink: 0; +} + +.navbar-links { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + width: 100%; +} + +.navbar-primary, +.navbar-auth { + display: flex; + align-items: center; + gap: 14px; +} + +.navbar-auth { + justify-content: flex-end; +} + +.nav-link { + position: relative; + font-size: 0.9rem; + font-weight: 600; + color: #1a1a1a; + padding: 8px 10px; + border-radius: 8px; + transition: color 0.2s, background 0.2s; +} + +.nav-link::after { + content: ""; + position: absolute; + left: 10px; + right: 10px; + bottom: 6px; + height: 2px; + background: #1a1a1a; + transform: scaleX(0); + transform-origin: center; + transition: transform 0.2s; +} + +.nav-link:hover { + background: #f5f5f5; +} + +.nav-link:hover::after { + transform: scaleX(1); +} + +.nav-btn { + display: inline-block; + padding: 9px 18px; + background: #1a1a1a; + color: #fff; + font-size: 0.85rem; + font-weight: 600; + border: 1px solid #1a1a1a; + border-radius: 999px; + transition: background 0.2s, color 0.2s, transform 0.2s; +} + +.nav-btn:hover { + background: #333; + transform: translateY(-1px); +} + +.nav-btn-outline { + background: #fff; + color: #1a1a1a; +} + +.nav-btn-outline:hover { + background: #1a1a1a; + color: #fff; +} + +.nav-user { + font-size: 0.82rem; + color: #444; + background: #f3f3f3; + border: 1px solid #e5e5e5; + border-radius: 999px; + padding: 7px 12px; +} + +/* ===== BUTTONS ===== */ +.btn { + display: inline-block; + padding: 10px 24px; + font-size: 0.9rem; + font-weight: 500; + border-radius: 6px; + border: 1px solid transparent; + transition: all 0.2s; + text-align: center; +} + +.btn-dark { + background: #1a1a1a; + color: #fff; + border-color: #1a1a1a; +} +.btn-dark:hover { background: #333; } + +.btn-outline { + background: #fff; + color: #1a1a1a; + border-color: #d0d0d0; +} +.btn-outline:hover { border-color: #1a1a1a; } + +.btn-danger { + background: #dc3545; + color: #fff; + border-color: #dc3545; +} +.btn-danger:hover { background: #c82333; } + +.btn-full { width: 100%; } + +/* ===== HERO ===== */ +.hero { + background: #f5f5f5; + padding: 80px 0; + text-align: center; +} + +.hero-content { max-width: 600px; margin: 0 auto; } + +.hero-title { + font-family: 'Playfair Display', serif; + font-size: 3rem; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 12px; +} + +.hero-subtitle { + font-size: 1.1rem; + color: #555; + margin-bottom: 32px; +} + +.hero-search { + display: flex; + gap: 0; + max-width: 440px; + margin: 0 auto; + border: 1px solid #d0d0d0; + border-radius: 8px; + overflow: hidden; + background: #fff; +} + +.hero-search-input { + flex: 1; + padding: 12px 16px; + border: none; + font-size: 0.9rem; + font-family: inherit; + outline: none; +} + +.hero-search-btn { + padding: 12px 24px; + background: #1a1a1a; + color: #fff; + border: none; + font-size: 0.85rem; + font-weight: 500; + font-family: inherit; +} + +.hero-search-btn:hover { background: #333; } + +/* ===== SECTIONS ===== */ +.section { padding: 60px 0; } +.section-gray { background: #f9f9f9; } + +.section-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 32px; +} + +.section-title { + font-family: 'Playfair Display', serif; + font-size: 1.75rem; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 4px; +} + +.section-subtitle { + font-size: 0.9rem; + color: #777; +} + +.more-link { + font-size: 0.9rem; + font-weight: 500; + color: #1a1a1a; +} + +.more-link:hover { text-decoration: underline; } + +.page-title { + font-family: 'Playfair Display', serif; + font-size: 2rem; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 8px; +} + +.page-subtitle { + font-size: 0.95rem; + color: #777; + margin-bottom: 32px; +} + +/* ===== PLANT CARDS ===== */ +.plants-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.plant-card { + background: #fff; + border: 1px solid #eee; + border-radius: 8px; + overflow: hidden; + transition: box-shadow 0.2s; + display: flex; + flex-direction: column; +} + +.plant-card:hover { + box-shadow: 0 4px 20px rgba(0,0,0,0.08); +} + +.plant-card-img { + height: 200px; + overflow: hidden; + background: #f5f5f5; +} + +.plant-card-img img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.plant-card-body { + padding: 16px; +} + +.plant-card-title { + font-size: 0.95rem; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 4px; +} + +.plant-card-desc { + font-size: 0.8rem; + color: #999; + margin-bottom: 4px; +} + +.plant-card-category { + font-size: 0.8rem; + font-weight: 600; + color: #1a1a1a; +} + +.plant-card-countries { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +/* ===== COUNTRY TAGS ===== */ +.country-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + background: #f0f0f0; + border-radius: 20px; + font-size: 0.72rem; + font-weight: 500; + color: #444; + white-space: nowrap; +} + +.country-tag-link { + transition: background 0.2s, color 0.2s; + text-decoration: none; +} + +.country-tag-link:hover { + background: #1a1a1a; + color: #fff; +} + +.country-flag { + width: 18px; + height: 12px; + object-fit: contain; + background: #fff; + border: 1px solid #ddd; + border-radius: 2px; + display: inline-block; + flex-shrink: 0; +} + +/* ===== DETAIL COUNTRIES ===== */ +.detail-countries { + margin-bottom: 20px; +} + +.detail-countries-label { + font-size: 0.85rem; + font-weight: 500; + color: #555; + margin-bottom: 8px; +} + +.country-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +/* ===== COUNTRY PAGE HEADER ===== */ +.country-header { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 32px; +} + +.country-header-flag { + width: 64px; + height: 44px; + object-fit: contain; + background: #fff; + border-radius: 4px; + border: 1px solid #e5e5e5; +} + +/* ===== COUNTRIES CHECKBOX LIST ===== */ +.countries-checkbox-list { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px 16px; + padding: 12px 16px; + border: 1px solid #d0d0d0; + border-radius: 8px; + background: #fafafa; + max-height: 220px; + overflow-y: auto; +} + +.country-checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + color: #333; + cursor: pointer; + margin-bottom: 0; +} + +.country-checkbox-label input[type="checkbox"] { + width: 15px; + height: 15px; + cursor: pointer; + accent-color: #1a1a1a; + flex-shrink: 0; +} + +/* ===== DETAIL PAGE ===== */ +.detail-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: start; +} + +.detail-image { + border-radius: 8px; + overflow: hidden; +} + +.detail-image img { + width: 100%; + height: 400px; + object-fit: cover; +} + +.detail-title { + font-family: 'Playfair Display', serif; + font-size: 2rem; + font-weight: 700; + margin-bottom: 8px; +} + +.detail-category { + font-size: 0.9rem; + color: #777; + margin-bottom: 20px; +} + +.detail-desc { + font-size: 0.95rem; + line-height: 1.8; + color: #444; + margin-bottom: 24px; +} + +.detail-specs { + margin-bottom: 24px; +} + +.detail-specs p { + font-size: 0.9rem; + margin-bottom: 6px; + color: #333; +} + +.detail-specs strong { + color: #1a1a1a; +} + +.detail-actions { + display: flex; + gap: 12px; +} + +/* ===== REVIEWS ===== */ +.reviews-title { + font-size: 1.2rem; + font-weight: 600; + margin: 28px 0 14px; + color: #1a1a1a; + border-bottom: 2px solid #e8e8e8; + padding-bottom: 8px; +} + +.comments-title { + font-size: 1.2rem; + font-weight: 600; + margin: 28px 0 14px; + color: #1a1a1a; + border-bottom: 2px solid #e8e8e8; + padding-bottom: 8px; +} + +.review-form { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 24px; +} + +.review-textarea { + width: 100%; + padding: 10px 14px; + border: 1px solid #d0d0d0; + border-radius: 8px; + font-size: 0.9rem; + font-family: inherit; + color: #1a1a1a; + resize: vertical; + transition: border-color 0.2s; +} + +.review-textarea:focus { + outline: none; + border-color: #1a1a1a; +} + +.review-select { + width: 100%; + padding: 10px 14px; + border: 1px solid #d0d0d0; + border-radius: 8px; + font-size: 0.9rem; + font-family: inherit; + color: #1a1a1a; + background: #fff; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 36px; + cursor: pointer; + transition: border-color 0.2s; +} + +.review-select:focus { + outline: none; + border-color: #1a1a1a; +} + +.reviews-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; +} + +.review-item { + background: #f9f9f9; + border: 1px solid #ebebeb; + border-radius: 10px; + padding: 14px 16px; +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.review-stars { + font-size: 1.1rem; + color: #f5a623; + letter-spacing: 2px; +} + +.review-date { + font-size: 0.78rem; + color: #888; +} + +.review-text { + font-size: 0.9rem; + color: #444; + margin: 0; + line-height: 1.5; +} + +.no-reviews { + color: #999; + font-size: 0.9rem; + font-style: italic; +} +.form-container { + max-width: 680px; +} + +.auth-box { + max-width: 440px; + margin: 0 auto; +} + +.auth-link { + text-align: center; + margin-top: 20px; + font-size: 0.9rem; +} + +.auth-link a { + color: #1a1a1a; + font-weight: 600; + text-decoration: underline; +} + +.comment-form-box { + background: #f9f9f9; + border: 1px solid #eee; + border-radius: 8px; + padding: 20px; + margin-bottom: 24px; +} + +.login-prompt { + background: #f9f9f9; + border: 1px solid #eee; + border-radius: 8px; + padding: 24px; + text-align: center; + margin-bottom: 24px; +} + +.login-prompt a { + color: #1a1a1a; + font-weight: 600; + text-decoration: underline; +} + +.comments-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.comment-card { + border: 1px solid #eee; + border-radius: 8px; + padding: 16px; +} + +.comment-header { + display: flex; + justify-content: space-between; + margin-bottom: 8px; +} + +.comment-date { + font-size: 0.8rem; + color: #999; +} + +.comment-body { + font-size: 0.9rem; + color: #444; + line-height: 1.6; +} + +.styled-form input[type="text"], +.styled-form input[type="email"], +.styled-form input[type="password"], +.styled-form input[type="url"], +.styled-form input[type="number"], +.styled-form textarea, +.styled-form select { + width: 100%; + padding: 10px 14px; + border: 1px solid #d0d0d0; + border-radius: 6px; + font-size: 0.9rem; + font-family: inherit; + color: #1a1a1a; + background: #fff; + transition: border-color 0.2s; +} + +.styled-form input:focus, +.styled-form textarea:focus, +.styled-form select:focus { + outline: none; + border-color: #1a1a1a; +} + +.styled-form textarea { resize: vertical; } + +.styled-form select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 36px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 0.85rem; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 6px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-row-3 { + grid-template-columns: 1fr 1fr 1fr; +} + +.form-checkbox-group { + display: flex; + align-items: center; + gap: 8px; +} + +.form-checkbox-group label { + margin-bottom: 0; + font-size: 0.9rem; +} + +.form-actions { + display: flex; + gap: 12px; + margin-top: 8px; +} + +.error-text { + display: block; + color: #dc3545; + font-size: 0.8rem; + margin-top: 4px; +} + +.form-errors { + background: #fff0f0; + border: 1px solid #fcc; + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 20px; +} + +.success-msg { + background: #f0fff0; + border: 1px solid #b2e6b2; + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 24px; + color: #2d7a2d; + font-size: 0.9rem; +} + +.section-footer { + text-align: center; + margin-top: 56px; + margin-bottom: 16px; +} + +/* ===== MESSAGES BANNER ===== */ +.messages-banner { + max-width: 1140px; + margin: 16px auto 0; + padding: 0 24px; +} + +.message-alert { + padding: 14px 20px; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + margin-bottom: 8px; +} + +.message-success { + background: #f0fff0; + border: 1px solid #b2e6b2; + color: #2d7a2d; +} + +.message-error { + background: #fff0f0; + border: 1px solid #fcc; + color: #c0392b; +} + +/* ===== UPLOAD BUTTON ===== */ +.upload-btn { + display: inline-block; + padding: 10px 20px; + background: #1a1a1a; + color: #fff; + font-size: 0.85rem; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; +} + +.upload-btn:hover { background: #333; } + +.upload-input-hidden input[type="file"] { + margin-top: 8px; +} + +/* ===== FILTER BAR ===== */ +.filter-bar { + display: flex; + align-items: flex-end; + gap: 16px; + margin-bottom: 32px; + flex-wrap: wrap; +} + +.filter-group { flex: 1; min-width: 150px; } + +.filter-group label { + display: block; + font-size: 0.8rem; + font-weight: 500; + margin-bottom: 4px; + color: #555; +} + +.filter-group select { + width: 100%; + padding: 8px 12px; + border: 1px solid #d0d0d0; + border-radius: 6px; + font-size: 0.85rem; + font-family: inherit; + appearance: none; + background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E") no-repeat right 10px center; + padding-right: 32px; +} + +.filter-group input[type="text"] { + width: 100%; + padding: 8px 12px; + border: 1px solid #d0d0d0; + border-radius: 6px; + font-size: 0.85rem; + font-family: inherit; + color: #1a1a1a; + background: #fff; + outline: none; +} + +.filter-group input[type="text"]:focus { + border-color: #1a1a1a; +} + +/* ===== SEARCH BAR ===== */ +.search-bar { + display: flex; + gap: 12px; + margin-bottom: 32px; +} + +.search-input { + flex: 1; + padding: 10px 16px; + border: 1px solid #d0d0d0; + border-radius: 6px; + font-size: 0.9rem; + font-family: inherit; +} + +.search-input:focus { + outline: none; + border-color: #1a1a1a; +} + +/* ===== CONFIRM BOX ===== */ +.confirm-box { + max-width: 480px; + margin: 0 auto; + text-align: center; + border: 1px solid #eee; + border-radius: 8px; + padding: 40px; +} + +.confirm-box h1 { + font-family: 'Playfair Display', serif; + font-size: 1.5rem; + margin-bottom: 12px; +} + +.confirm-warning { + color: #dc3545; + font-size: 0.85rem; + margin-bottom: 24px; +} + +.confirm-plant { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: #f9f9f9; + border-radius: 6px; + margin-bottom: 24px; + text-align: left; +} + +.confirm-img { + width: 64px; + height: 64px; + object-fit: cover; + border-radius: 6px; +} + +.confirm-actions { + display: flex; + gap: 12px; + justify-content: center; +} + +/* ===== CONTACT PAGE ===== */ +.contact-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: start; +} + +.contact-img { + width: 100%; + height: 100%; + min-height: 400px; + object-fit: cover; + border-radius: 8px; +} + +/* ===== MESSAGES PAGE ===== */ +.messages-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; +} + +.message-card { + border: 1px solid #eee; + border-radius: 8px; + padding: 24px; +} + +.message-icon { + font-size: 1.2rem; + color: #999; + margin-bottom: 12px; +} + +.message-name { + font-size: 1rem; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 4px; +} + +.message-email { + font-size: 0.85rem; + color: #2563eb; + display: block; + margin-bottom: 12px; +} + +.message-body { + font-size: 0.85rem; + color: #555; + line-height: 1.6; +} + +/* ===== EMPTY STATE ===== */ +.empty-state { + text-align: center; + padding: 60px 24px; +} + +.empty-state h2 { + font-family: 'Playfair Display', serif; + font-size: 1.3rem; + margin-bottom: 8px; +} + +.empty-state p { + color: #777; + font-size: 0.9rem; +} + +.empty-state a { color: #1a1a1a; text-decoration: underline; } +.empty-text { color: #777; font-size: 0.9rem; } +.empty-text a { color: #1a1a1a; text-decoration: underline; } + +/* ===== FOOTER ===== */ +.footer { + background: #fff; + border-top: 1px solid #e5e5e5; + padding: 48px 0 32px; + margin-top: 40px; +} + +.footer-grid { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + flex-wrap: nowrap; + gap: 40px; + margin-bottom: 32px; +} + + +.footer-col h3 { + font-size: 1rem; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; +} + + +.footer-heading { + font-size: 0.85rem; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 12px; +} + +.footer-col a { + display: block; + font-size: 0.85rem; + color: #777; + padding: 3px 0; + transition: color 0.2s; +} + +.footer-col a:hover { color: #1a1a1a; } + +.footer-bottom { + border-top: 1px solid #e5e5e5; + padding-top: 20px; +} + +.footer-social { + display: flex; + gap: 12px; +} + +.social-icon { + width: 24px; + height: 24px; + font-size: 8px; + color: #999; +} + +/* ============================================ + RESPONSIVE + ============================================ */ + +/* ── Tablet (≤ 900px) ── */ +@media (max-width: 900px) { + .container { padding: 0 20px; } + + /* Navbar */ + .navbar-inner { min-height: 60px; } + .navbar-logo { font-size: 1.1rem; } + .navbar-links { gap: 12px; } + .navbar-primary, + .navbar-auth { gap: 8px; } + .nav-link { font-size: 0.85rem; } + .nav-btn { padding: 7px 14px; font-size: 0.8rem; } + .nav-user { font-size: 0.78rem; padding: 6px 10px; } + + /* Hero */ + .hero { padding: 60px 0; } + .hero-title { font-size: 2.4rem; } + .hero-subtitle { font-size: 1rem; } + + /* Grids */ + .plants-grid { grid-template-columns: repeat(2, 1fr); gap: 20px; } + .messages-grid { grid-template-columns: 1fr 1fr; gap: 20px; } + + /* Detail */ + .detail-layout { grid-template-columns: 1fr; gap: 28px; } + .detail-image img { height: 320px; } + + /* Contact */ + .contact-layout { grid-template-columns: 1fr; } + .contact-image-side { max-height: 300px; overflow: hidden; border-radius: 8px; } + .contact-img { height: 300px; } + + /* Footer */ + .footer-grid { grid-template-columns: 1fr 1fr; gap: 28px; } + + /* Forms */ + .form-row-3 { grid-template-columns: 1fr 1fr; } + .form-container { max-width: 100%; } +} + +/* ── Mobile (≤ 640px) ── */ +@media (max-width: 640px) { + .container { padding: 0 16px; } + + /* Navbar */ + .navbar-inner { + min-height: 52px; + flex-wrap: wrap; + padding: 10px 0; + } + .navbar-links { + width: 100%; + flex-direction: column; + align-items: stretch; + gap: 8px; + } + .navbar-primary, + .navbar-auth { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + gap: 8px; + } + .nav-link { font-size: 0.8rem; padding: 7px 9px; } + .nav-btn { padding: 6px 12px; font-size: 0.78rem; } + .nav-user { font-size: 0.78rem; } + .navbar-logo { font-size: 1rem; } + + /* Hero */ + .hero { padding: 40px 0; } + .hero-title { font-size: 2rem; } + .hero-subtitle { font-size: 0.9rem; margin-bottom: 24px; } + .hero-search { max-width: 100%; } + .hero-search-input { font-size: 0.85rem; padding: 10px 12px; } + .hero-search-btn { padding: 10px 16px; font-size: 0.8rem; } + + /* Sections */ + .section { padding: 36px 0; } + .section-title { font-size: 1.4rem; } + .section-header { flex-direction: column; align-items: flex-start; gap: 6px; } + .section-footer { margin-top: 36px; } + + /* Page titles */ + .page-title { font-size: 1.5rem; } + .page-subtitle { font-size: 0.85rem; } + + /* Grids */ + .plants-grid { grid-template-columns: 1fr; gap: 16px; } + .messages-grid { grid-template-columns: 1fr; gap: 16px; } + + /* Plant cards */ + .plant-card-img { height: 180px; } + + /* Detail */ + .detail-layout { gap: 20px; } + .detail-image img { height: 240px; border-radius: 8px; } + .detail-title { font-size: 1.5rem; } + .detail-actions { flex-direction: column; gap: 10px; } + .detail-actions .btn { width: 100%; text-align: center; } + + /* Filter */ + .filter-bar { flex-direction: column; align-items: stretch; gap: 12px; } + .filter-group { min-width: unset; } + + /* Search */ + .search-bar { flex-direction: column; gap: 10px; } + .search-input { width: 100%; } + + /* Forms */ + .form-row { grid-template-columns: 1fr; } + .form-row-3 { grid-template-columns: 1fr; } + .form-actions { flex-direction: column; gap: 10px; } + .form-actions .btn { width: 100%; text-align: center; } + .form-container { max-width: 100%; } + + /* Contact */ + .contact-image-side { display: none; } + .contact-layout { grid-template-columns: 1fr; } + + /* Confirm */ + .confirm-box { padding: 24px 16px; } + .confirm-actions { flex-direction: column; gap: 10px; } + .confirm-actions .btn { width: 100%; } + + /* Footer */ + .footer { padding: 36px 0 24px; } + .footer-grid { grid-template-columns: 1fr; gap: 20px; } + .footer-social { justify-content: flex-start; } + + /* Messages banner */ + .messages-banner { padding: 0 16px; } +} + +/* ── Small Mobile (≤ 400px) ── */ +@media (max-width: 400px) { + .hero-title { font-size: 1.7rem; } + .navbar-links { gap: 6px; } + .nav-btn { padding: 5px 10px; font-size: 0.75rem; } +} + +.hero { + background-image: url('../images/نبتة خلفية.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} \ No newline at end of file diff --git a/Planteer/static/images/sa.svg b/Planteer/static/images/sa.svg new file mode 100644 index 0000000..596cf48 --- /dev/null +++ b/Planteer/static/images/sa.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Planteer/static/images/saudi.svg b/Planteer/static/images/saudi.svg new file mode 100644 index 0000000..596cf48 --- /dev/null +++ b/Planteer/static/images/saudi.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/Planteer/static/images/\330\265\331\210\330\261\330\251 \331\204\331\204\330\256\331\204\331\201\331\212\330\251 \331\206\330\250\330\247\330\252\330\247\330\252.jpg" "b/Planteer/static/images/\330\265\331\210\330\261\330\251 \331\204\331\204\330\256\331\204\331\201\331\212\330\251 \331\206\330\250\330\247\330\252\330\247\330\252.jpg" new file mode 100644 index 0000000..4e79e2d Binary files /dev/null and "b/Planteer/static/images/\330\265\331\210\330\261\330\251 \331\204\331\204\330\256\331\204\331\201\331\212\330\251 \331\206\330\250\330\247\330\252\330\247\330\252.jpg" differ diff --git "a/Planteer/static/images/\331\206\330\250\330\252\330\251 \330\256\331\204\331\201\331\212\330\251.jpg" "b/Planteer/static/images/\331\206\330\250\330\252\330\251 \330\256\331\204\331\201\331\212\330\251.jpg" new file mode 100644 index 0000000..3b08671 Binary files /dev/null and "b/Planteer/static/images/\331\206\330\250\330\252\330\251 \330\256\331\204\331\201\331\212\330\251.jpg" differ diff --git a/Planteer/templates/base.html b/Planteer/templates/base.html new file mode 100644 index 0000000..a98c771 --- /dev/null +++ b/Planteer/templates/base.html @@ -0,0 +1,95 @@ +{% load static %} + + + + + + {% block title %}Planteer{% endblock %} + + + + + + + + + + + +
+ {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% block content %}{% endblock %} +
+ + + + + + + \ No newline at end of file