Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f3db50aa8 |
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
3
ai/__init__.py
Normal file
3
ai/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Helpers for interacting with the Flatlogic AI proxy from Django code."""
|
||||||
|
|
||||||
|
from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401
|
||||||
420
ai/local_ai_api.py
Normal file
420
ai/local_ai_api.py
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
"""
|
||||||
|
LocalAIApi — lightweight Python client for the Flatlogic AI proxy.
|
||||||
|
|
||||||
|
Usage (inside the Django workspace):
|
||||||
|
|
||||||
|
from ai.local_ai_api import LocalAIApi
|
||||||
|
|
||||||
|
response = LocalAIApi.create_response({
|
||||||
|
"input": [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "Summarise this text in two sentences."},
|
||||||
|
],
|
||||||
|
"text": {"format": {"type": "json_object"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
if response.get("success"):
|
||||||
|
data = LocalAIApi.decode_json_from_response(response)
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Typical successful payload (truncated):
|
||||||
|
# {
|
||||||
|
# "id": "resp_xxx",
|
||||||
|
# "status": "completed",
|
||||||
|
# "output": [
|
||||||
|
# {"type": "reasoning", "summary": []},
|
||||||
|
# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
||||||
|
# ],
|
||||||
|
# "usage": { "input_tokens": 123, "output_tokens": 456 }
|
||||||
|
# }
|
||||||
|
|
||||||
|
The helper automatically injects the project UUID header and falls back to
|
||||||
|
reading executor/.env if environment variables are missing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import ssl
|
||||||
|
from typing import Any, Dict, Iterable, Optional
|
||||||
|
from urllib import error as urlerror
|
||||||
|
from urllib import request as urlrequest
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LocalAIApi",
|
||||||
|
"create_response",
|
||||||
|
"request",
|
||||||
|
"fetch_status",
|
||||||
|
"await_response",
|
||||||
|
"extract_text",
|
||||||
|
"decode_json_from_response",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_CONFIG_CACHE: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LocalAIApi:
|
||||||
|
"""Static helpers mirroring the PHP implementation."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
return create_response(params, options or {})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None,
|
||||||
|
options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
return request(path, payload or {}, options or {})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_text(response: Dict[str, Any]) -> str:
|
||||||
|
return extract_text(response)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
return decode_json_from_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""Signature compatible with the OpenAI Responses API."""
|
||||||
|
options = options or {}
|
||||||
|
payload = dict(params)
|
||||||
|
|
||||||
|
if not isinstance(payload.get("input"), list) or not payload["input"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "input_missing",
|
||||||
|
"message": 'Parameter "input" is required and must be a non-empty list.',
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = _config()
|
||||||
|
if not payload.get("model"):
|
||||||
|
payload["model"] = cfg["default_model"]
|
||||||
|
|
||||||
|
initial = request(options.get("path"), payload, options)
|
||||||
|
if not initial.get("success"):
|
||||||
|
return initial
|
||||||
|
|
||||||
|
data = initial.get("data")
|
||||||
|
if isinstance(data, dict) and "ai_request_id" in data:
|
||||||
|
ai_request_id = data["ai_request_id"]
|
||||||
|
poll_timeout = int(options.get("poll_timeout", 300))
|
||||||
|
poll_interval = int(options.get("poll_interval", 5))
|
||||||
|
return await_response(ai_request_id, {
|
||||||
|
"interval": poll_interval,
|
||||||
|
"timeout": poll_timeout,
|
||||||
|
"headers": options.get("headers"),
|
||||||
|
"timeout_per_call": options.get("timeout"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
|
||||||
|
def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""Perform a raw request to the AI proxy."""
|
||||||
|
cfg = _config()
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
resolved_path = path or options.get("path") or cfg["responses_path"]
|
||||||
|
if not resolved_path:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "project_id_missing",
|
||||||
|
"message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.",
|
||||||
|
}
|
||||||
|
|
||||||
|
project_uuid = cfg["project_uuid"]
|
||||||
|
if not project_uuid:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "project_uuid_missing",
|
||||||
|
"message": "PROJECT_UUID is not defined; aborting AI request.",
|
||||||
|
}
|
||||||
|
|
||||||
|
if "project_uuid" not in payload and project_uuid:
|
||||||
|
payload["project_uuid"] = project_uuid
|
||||||
|
|
||||||
|
url = _build_url(resolved_path, cfg["base_url"])
|
||||||
|
opt_timeout = options.get("timeout")
|
||||||
|
timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout)
|
||||||
|
verify_tls = options.get("verify_tls", cfg["verify_tls"])
|
||||||
|
|
||||||
|
headers: Dict[str, str] = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
cfg["project_header"]: project_uuid,
|
||||||
|
}
|
||||||
|
extra_headers = options.get("headers")
|
||||||
|
if isinstance(extra_headers, Iterable):
|
||||||
|
for header in extra_headers:
|
||||||
|
if isinstance(header, str) and ":" in header:
|
||||||
|
name, value = header.split(":", 1)
|
||||||
|
headers[name.strip()] = value.strip()
|
||||||
|
|
||||||
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
return _http_request(url, "POST", body, headers, timeout, verify_tls)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""Fetch status for a queued AI request."""
|
||||||
|
cfg = _config()
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
project_uuid = cfg["project_uuid"]
|
||||||
|
if not project_uuid:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "project_uuid_missing",
|
||||||
|
"message": "PROJECT_UUID is not defined; aborting status check.",
|
||||||
|
}
|
||||||
|
|
||||||
|
status_path = _resolve_status_path(ai_request_id, cfg)
|
||||||
|
url = _build_url(status_path, cfg["base_url"])
|
||||||
|
|
||||||
|
opt_timeout = options.get("timeout")
|
||||||
|
timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout)
|
||||||
|
verify_tls = options.get("verify_tls", cfg["verify_tls"])
|
||||||
|
|
||||||
|
headers: Dict[str, str] = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
cfg["project_header"]: project_uuid,
|
||||||
|
}
|
||||||
|
extra_headers = options.get("headers")
|
||||||
|
if isinstance(extra_headers, Iterable):
|
||||||
|
for header in extra_headers:
|
||||||
|
if isinstance(header, str) and ":" in header:
|
||||||
|
name, value = header.split(":", 1)
|
||||||
|
headers[name.strip()] = value.strip()
|
||||||
|
|
||||||
|
return _http_request(url, "GET", None, headers, timeout, verify_tls)
|
||||||
|
|
||||||
|
|
||||||
|
def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""Poll status endpoint until the request is complete or timed out."""
|
||||||
|
options = options or {}
|
||||||
|
timeout = int(options.get("timeout", 300))
|
||||||
|
interval = int(options.get("interval", 5))
|
||||||
|
if interval <= 0:
|
||||||
|
interval = 5
|
||||||
|
per_call_timeout = options.get("timeout_per_call")
|
||||||
|
|
||||||
|
deadline = time.time() + max(timeout, interval)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
status_resp = fetch_status(ai_request_id, {
|
||||||
|
"headers": options.get("headers"),
|
||||||
|
"timeout": per_call_timeout,
|
||||||
|
"verify_tls": options.get("verify_tls"),
|
||||||
|
})
|
||||||
|
if status_resp.get("success"):
|
||||||
|
data = status_resp.get("data") or {}
|
||||||
|
if isinstance(data, dict):
|
||||||
|
status_value = data.get("status")
|
||||||
|
if status_value == "success":
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": 200,
|
||||||
|
"data": data.get("response", data),
|
||||||
|
}
|
||||||
|
if status_value == "failed":
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"status": 500,
|
||||||
|
"error": str(data.get("error") or "AI request failed"),
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return status_resp
|
||||||
|
|
||||||
|
if time.time() >= deadline:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "timeout",
|
||||||
|
"message": "Timed out waiting for AI response.",
|
||||||
|
}
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_text(response: Dict[str, Any]) -> str:
|
||||||
|
"""Public helper to extract plain text from a Responses payload."""
|
||||||
|
return _extract_text(response)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Attempt to decode JSON emitted by the model (handles markdown fences)."""
|
||||||
|
text = _extract_text(response)
|
||||||
|
if text == "":
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded = json.loads(text)
|
||||||
|
if isinstance(decoded, dict):
|
||||||
|
return decoded
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
stripped = text.strip()
|
||||||
|
if stripped.startswith("```json"):
|
||||||
|
stripped = stripped[7:]
|
||||||
|
if stripped.endswith("```"):
|
||||||
|
stripped = stripped[:-3]
|
||||||
|
stripped = stripped.strip()
|
||||||
|
if stripped and stripped != text:
|
||||||
|
try:
|
||||||
|
decoded = json.loads(stripped)
|
||||||
|
if isinstance(decoded, dict):
|
||||||
|
return decoded
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_text(response: Dict[str, Any]) -> str:
|
||||||
|
payload = response.get("data") if response.get("success") else response.get("response")
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
output = payload.get("output")
|
||||||
|
if isinstance(output, list):
|
||||||
|
combined = ""
|
||||||
|
for item in output:
|
||||||
|
content = item.get("content") if isinstance(item, dict) else None
|
||||||
|
if isinstance(content, list):
|
||||||
|
for block in content:
|
||||||
|
if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"):
|
||||||
|
combined += str(block["text"])
|
||||||
|
if combined:
|
||||||
|
return combined
|
||||||
|
choices = payload.get("choices")
|
||||||
|
if isinstance(choices, list) and choices:
|
||||||
|
message = choices[0].get("message")
|
||||||
|
if isinstance(message, dict) and message.get("content"):
|
||||||
|
return str(message["content"])
|
||||||
|
if isinstance(payload, str):
|
||||||
|
return payload
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _config() -> Dict[str, Any]:
|
||||||
|
global _CONFIG_CACHE # noqa: PLW0603
|
||||||
|
if _CONFIG_CACHE is not None:
|
||||||
|
return _CONFIG_CACHE
|
||||||
|
|
||||||
|
_ensure_env_loaded()
|
||||||
|
|
||||||
|
base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com")
|
||||||
|
project_id = os.getenv("PROJECT_ID") or None
|
||||||
|
responses_path = os.getenv("AI_RESPONSES_PATH")
|
||||||
|
if not responses_path and project_id:
|
||||||
|
responses_path = f"/projects/{project_id}/ai-request"
|
||||||
|
|
||||||
|
_CONFIG_CACHE = {
|
||||||
|
"base_url": base_url,
|
||||||
|
"responses_path": responses_path,
|
||||||
|
"project_id": project_id,
|
||||||
|
"project_uuid": os.getenv("PROJECT_UUID"),
|
||||||
|
"project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"),
|
||||||
|
"default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5-mini"),
|
||||||
|
"timeout": int(os.getenv("AI_TIMEOUT", "30")),
|
||||||
|
"verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"},
|
||||||
|
}
|
||||||
|
return _CONFIG_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
def _build_url(path: str, base_url: str) -> str:
|
||||||
|
trimmed = path.strip()
|
||||||
|
if trimmed.startswith("http://") or trimmed.startswith("https://"):
|
||||||
|
return trimmed
|
||||||
|
if trimmed.startswith("/"):
|
||||||
|
return f"{base_url}{trimmed}"
|
||||||
|
return f"{base_url}/{trimmed}"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
|
||||||
|
base_path = (cfg.get("responses_path") or "").rstrip("/")
|
||||||
|
if not base_path:
|
||||||
|
return f"/ai-request/{ai_request_id}/status"
|
||||||
|
if not base_path.endswith("/ai-request"):
|
||||||
|
base_path = f"{base_path}/ai-request"
|
||||||
|
return f"{base_path}/{ai_request_id}/status"
|
||||||
|
|
||||||
|
|
||||||
|
def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str],
|
||||||
|
timeout: int, verify_tls: bool) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Shared HTTP helper for GET/POST requests.
|
||||||
|
"""
|
||||||
|
req = urlrequest.Request(url, data=body, method=method.upper())
|
||||||
|
for name, value in headers.items():
|
||||||
|
req.add_header(name, value)
|
||||||
|
|
||||||
|
context = None
|
||||||
|
if not verify_tls:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urlrequest.urlopen(req, timeout=timeout, context=context) as resp:
|
||||||
|
status = resp.getcode()
|
||||||
|
response_body = resp.read().decode("utf-8", errors="replace")
|
||||||
|
except urlerror.HTTPError as exc:
|
||||||
|
status = exc.getcode()
|
||||||
|
response_body = exc.read().decode("utf-8", errors="replace")
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "request_failed",
|
||||||
|
"message": str(exc),
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded = None
|
||||||
|
if response_body:
|
||||||
|
try:
|
||||||
|
decoded = json.loads(response_body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
decoded = None
|
||||||
|
|
||||||
|
if 200 <= status < 300:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": status,
|
||||||
|
"data": decoded if decoded is not None else response_body,
|
||||||
|
}
|
||||||
|
|
||||||
|
error_message = "AI proxy request failed"
|
||||||
|
if isinstance(decoded, dict):
|
||||||
|
error_message = decoded.get("error") or decoded.get("message") or error_message
|
||||||
|
elif response_body:
|
||||||
|
error_message = response_body
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"status": status,
|
||||||
|
"error": error_message,
|
||||||
|
"response": decoded if decoded is not None else response_body,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_env_loaded() -> None:
|
||||||
|
"""Populate os.environ from executor/.env if variables are missing."""
|
||||||
|
if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"):
|
||||||
|
return
|
||||||
|
|
||||||
|
env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
|
||||||
|
if not os.path.exists(env_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(env_path, "r", encoding="utf-8") as handle:
|
||||||
|
for line in handle:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||||
|
continue
|
||||||
|
key, value = stripped.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip('\'"')
|
||||||
|
if key and not os.getenv(key):
|
||||||
|
os.environ[key] = value
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
BIN
assets/pasted-20251124-055640-53e89814.webp
Normal file
BIN
assets/pasted-20251124-055640-53e89814.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 573 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Ticket
|
from .models import MembershipApplication
|
||||||
|
|
||||||
@admin.register(Ticket)
|
@admin.register(MembershipApplication)
|
||||||
class TicketAdmin(admin.ModelAdmin):
|
class MembershipApplicationAdmin(admin.ModelAdmin):
|
||||||
list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at')
|
list_display = ('name', 'email', 'desired_role', 'status', 'created_at')
|
||||||
list_filter = ('status', 'priority')
|
list_filter = ('status', 'desired_role')
|
||||||
search_fields = ('subject', 'requester_email', 'description')
|
search_fields = ('name', 'email', 'statement')
|
||||||
@ -1,7 +1,12 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Ticket
|
from .models import MembershipApplication
|
||||||
|
|
||||||
class TicketForm(forms.ModelForm):
|
class MembershipApplicationForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = MembershipApplication
|
||||||
fields = ['subject', 'requester_email', 'priority', 'description']
|
fields = ['name', 'email', 'desired_role', 'statement']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'placeholder': 'Your Full Name'}),
|
||||||
|
'email': forms.EmailInput(attrs={'placeholder': 'Your Email Address'}),
|
||||||
|
'statement': forms.Textarea(attrs={'rows': 5, 'placeholder': 'Tell us a bit about yourself and your interest in joining the network.'}),
|
||||||
|
}
|
||||||
28
core/migrations/0002_membershipapplication_delete_ticket.py
Normal file
28
core/migrations/0002_membershipapplication_delete_ticket.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-23 17:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MembershipApplication',
|
||||||
|
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, unique=True)),
|
||||||
|
('desired_role', models.CharField(choices=[('FARMER', 'Farmer'), ('DRIVER', 'Driver'), ('COMMUNITY', 'Community Member')], max_length=20)),
|
||||||
|
('statement', models.TextField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('status', models.CharField(default='PENDING', max_length=20)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Ticket',
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,25 +1,17 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Ticket(models.Model):
|
class MembershipApplication(models.Model):
|
||||||
STATUS_CHOICES = [
|
ROLE_CHOICES = [
|
||||||
('open', 'Open'),
|
('FARMER', 'Farmer'),
|
||||||
('in_progress', 'In Progress'),
|
('DRIVER', 'Driver'),
|
||||||
('closed', 'Closed'),
|
('COMMUNITY', 'Community Member'),
|
||||||
]
|
]
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
PRIORITY_CHOICES = [
|
email = models.EmailField(unique=True)
|
||||||
('low', 'Low'),
|
desired_role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
||||||
('medium', 'Medium'),
|
statement = models.TextField()
|
||||||
('high', 'High'),
|
|
||||||
]
|
|
||||||
|
|
||||||
subject = models.CharField(max_length=255)
|
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
|
|
||||||
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
|
||||||
requester_email = models.EmailField()
|
|
||||||
description = models.TextField()
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
status = models.CharField(max_length=20, default='PENDING') # PENDING, APPROVED, REJECTED
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.subject
|
return f"{self.name} ({self.email})"
|
||||||
|
|||||||
50
core/templates/base.html
Normal file
50
core/templates/base.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Farm-to-Table Sovereignty Network{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&family=Roboto+Slab:wght@700&display=swap" rel="stylesheet">
|
||||||
|
{% load static %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'home' %}">FTSN</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'home' %}#about">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'home' %}#roles">Roles</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="btn btn-accent" href="{% url 'apply_for_membership' %}">Apply for Membership</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer mt-auto py-3">
|
||||||
|
<div class="container text-center">
|
||||||
|
<span class="text-muted">© 2025 Farm-to-Table Sovereignty Network. All Rights Reserved.</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
core/templates/core/application_success.html
Normal file
19
core/templates/core/application_success.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Application Received | {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container page-container text-center">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-card">
|
||||||
|
<h1 class="display-4">Thank You!</h1>
|
||||||
|
<p class="lead">Your membership application has been received.</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>We are excited about your interest in joining our community. Your application will be reviewed by an administrator, and you will be notified of the decision via email.</p>
|
||||||
|
<a class="btn btn-primary btn-lg mt-3" href="{% url 'home' %}" role="button">Return to Homepage</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
34
core/templates/core/apply.html
Normal file
34
core/templates/core/apply.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Apply for Membership | {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container page-container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-card">
|
||||||
|
<h1 class="text-center mb-4">Join the Network</h1>
|
||||||
|
<p class="text-center mb-5">Complete the form below to apply for membership. Applications are reviewed by the assembly administrators. We welcome sovereign individuals dedicated to mutual aid and community resilience.</p>
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Submit Application</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
14
core/templates/core/article_detail.html
Normal file
14
core/templates/core/article_detail.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ article.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h1>{{ article.title }}</h1>
|
||||||
|
<p class="text-muted">Published on {{ article.created_at|date:"F d, Y" }}</p>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
{{ article.content|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,157 +1,57 @@
|
|||||||
<!doctype html>
|
{% extends 'base.html' %}
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
{% block content %}
|
||||||
<meta charset="utf-8">
|
<div class="hero-section">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<div class="container">
|
||||||
<title>{{ project_name }}</title>
|
<h1 class="display-3">Farm-to-Table Sovereignty Network</h1>
|
||||||
{% if project_description %}
|
<p class="lead">Private cooperation among sovereign people for mutual aid and emergency preparedness.</p>
|
||||||
<meta name="description" content="{{ project_description }}">
|
<a href="{% url 'apply_for_membership' %}" class="btn btn-accent btn-lg">Apply for Membership</a>
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
|
||||||
{% endif %}
|
|
||||||
{% if project_image_url %}
|
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
|
||||||
{% endif %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color-start: #6a11cb;
|
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.08);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(130deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'><path d='M-20 20L160 20M20 -20L20 160' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 24s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate3d(-140px, -140px, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: clamp(2rem, 4vw, 3rem);
|
|
||||||
width: min(640px, 92vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: clamp(2rem, 4vw, 3rem);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.3rem, 3rem);
|
|
||||||
letter-spacing: -0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.6rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: rgba(15, 23, 42, 0.35);
|
|
||||||
padding: 0.2rem 0.6rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: 2.4rem;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
|
||||||
<span class="sr-only">Loading…</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p>Appwizzy AI is collecting your requirements and applying the first changes.</p>
|
</div>
|
||||||
<p>This page will refresh automatically as the plan is implemented.</p>
|
|
||||||
<p>
|
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> —
|
|
||||||
UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
<section id="about" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto text-center">
|
||||||
|
<h2 class="section-title">A Private Network for a Resilient Future</h2>
|
||||||
|
<p class="lead text-muted">We are a private, unincorporated association of living men and women operating under natural law. Our mission is to build a resilient food supply chain, independent of corporate and government systems, to support our communities, especially in times of crisis.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="roles" class="py-5 bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h2 class="section-title">Our Member Roles</h2>
|
||||||
|
<p class="lead text-muted">Each member plays a vital part in the strength of our network.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 text-center p-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Farmers</h3>
|
||||||
|
<p class="card-text">Post available food and produce from your private land. Set your own terms for private exchange and connect directly with other members.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 text-center p-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Drivers</h3>
|
||||||
|
<p class="card-text">Offer transport assistance using your private vehicle. Help move resources between farmers and communities through private agreements.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 text-center p-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Community</h3>
|
||||||
|
<p class="card-text">Access available food through private exchange. Connect with local farmers and drivers to ensure your family and community have what they need.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from .views import home, apply_for_membership, application_success
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("apply/", apply_for_membership, name="apply_for_membership"),
|
||||||
|
path("application-success/", application_success, name="application_success"),
|
||||||
]
|
]
|
||||||
@ -1,37 +1,18 @@
|
|||||||
import os
|
from django.shortcuts import render, redirect
|
||||||
import platform
|
from .forms import MembershipApplicationForm
|
||||||
|
|
||||||
from django import get_version as django_version
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.views.generic.edit import CreateView
|
|
||||||
|
|
||||||
from .forms import TicketForm
|
|
||||||
from .models import Ticket
|
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
return render(request, "core/index.html")
|
||||||
host_name = request.get_host().lower()
|
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
|
||||||
now = timezone.now()
|
|
||||||
|
|
||||||
context = {
|
def apply_for_membership(request):
|
||||||
"project_name": "New Style",
|
if request.method == 'POST':
|
||||||
"agent_brand": agent_brand,
|
form = MembershipApplicationForm(request.POST)
|
||||||
"django_version": django_version(),
|
if form.is_valid():
|
||||||
"python_version": platform.python_version(),
|
form.save()
|
||||||
"current_time": now,
|
return redirect('application_success')
|
||||||
"host_name": host_name,
|
else:
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
form = MembershipApplicationForm()
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
return render(request, 'core/apply.html', {'form': form})
|
||||||
}
|
|
||||||
return render(request, "core/index.html", context)
|
|
||||||
|
|
||||||
|
def application_success(request):
|
||||||
class TicketCreateView(CreateView):
|
return render(request, 'core/application_success.html')
|
||||||
model = Ticket
|
|
||||||
form_class = TicketForm
|
|
||||||
template_name = "core/ticket_create.html"
|
|
||||||
success_url = reverse_lazy("home")
|
|
||||||
114
static/css/custom.css
Normal file
114
static/css/custom.css
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/* Farm-to-Table Sovereignty Network Custom Styles */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #2E4636; /* Deep Earth Green */
|
||||||
|
--secondary-color: #5D4037; /* Warm Soil Brown */
|
||||||
|
--accent-color: #FFC107; /* Sun-kissed Gold */
|
||||||
|
--neutral-bg: #F5F5DC; /* Light Parchment */
|
||||||
|
--text-color: #333333;
|
||||||
|
--light-text-color: #f8f9fa;
|
||||||
|
--heading-font: 'Roboto Slab', serif;
|
||||||
|
--body-font: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--body-font);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||||
|
font-family: var(--heading-font);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-family: var(--heading-font);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1e2d23;
|
||||||
|
border-color: #1e2d23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
border-color: #d39e00;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('/static/img/hero-background.webp') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
padding: 8rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section .lead {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 1rem auto 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roles .card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roles .card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.page-container {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card h1 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
min-height: 48px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(46, 70, 54, 0.25);
|
||||||
|
}
|
||||||
BIN
static/img/hero-background.webp
Normal file
BIN
static/img/hero-background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 573 KiB |
@ -1,21 +1,114 @@
|
|||||||
|
/* Farm-to-Table Sovereignty Network Custom Styles */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color-start: #6a11cb;
|
--primary-color: #2E4636; /* Deep Earth Green */
|
||||||
--bg-color-end: #2575fc;
|
--secondary-color: #5D4037; /* Warm Soil Brown */
|
||||||
--text-color: #ffffff;
|
--accent-color: #FFC107; /* Sun-kissed Gold */
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
--neutral-bg: #F5F5DC; /* Light Parchment */
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
--text-color: #333333;
|
||||||
|
--light-text-color: #f8f9fa;
|
||||||
|
--heading-font: 'Roboto Slab', serif;
|
||||||
|
--body-font: 'Roboto', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
font-family: var(--body-font);
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
display: flex;
|
background-color: #fff;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||||
text-align: center;
|
font-family: var(--heading-font);
|
||||||
overflow: hidden;
|
font-weight: 700;
|
||||||
position: relative;
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-family: var(--heading-font);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1e2d23;
|
||||||
|
border-color: #1e2d23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
border-color: #d39e00;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('/static/img/hero-background.webp') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
padding: 8rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section .lead {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 1rem auto 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roles .card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roles .card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.page-container {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card h1 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
min-height: 48px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(46, 70, 54, 0.25);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
staticfiles/img/hero-background.webp
Normal file
BIN
staticfiles/img/hero-background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 573 KiB |
Loading…
x
Reference in New Issue
Block a user