Initial commit: site code, layouts, content (images added separately)
Fixes duplicate placeholder cover image on the 3 newest blog posts, broken aspect-ratio classes on the albums listing page, and a non-responsive fixed sidebar on the chat page.
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
public/
|
||||
resources/
|
||||
.hugo_build.lock
|
||||
hugo_stats.json
|
||||
138
IMPLEMENTATION_SUMMARY.md
Normal file
138
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Membership Paywall System - Implementation Summary
|
||||
|
||||
## Files Created
|
||||
|
||||
### Backend
|
||||
- `scripts/membership_api.py` - Flask API server for membership management
|
||||
- `requirements.txt` - Python dependencies
|
||||
|
||||
### Frontend
|
||||
- `static/js/membership.js` - Client-side membership functionality
|
||||
- `layouts/membership/single.html` - Membership signup/login page
|
||||
- `layouts/testimonials/single.html` - Testimonials page with paywall
|
||||
- `layouts/admin/single.html` - Admin approval panel
|
||||
- `layouts/contact/single.html` - Contact page layout
|
||||
|
||||
### Content
|
||||
- `content/membership.md` - Membership page content
|
||||
- `content/testimonials.md` - Testimonials page content
|
||||
- `content/admin.md` - Admin panel page
|
||||
|
||||
### Documentation
|
||||
- `MEMBERSHIP_SETUP.md` - Setup and configuration guide
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `layouts/_default/baseof.html` - Added membership.js, auth UI, navigation updates
|
||||
- `layouts/library/list.html` - Added paywall and download tracking
|
||||
- `content/contact.md` - Paywalled email, secure form
|
||||
- `hugo.yaml` - Added membership and testimonials to menu
|
||||
|
||||
## Key Features Implemented
|
||||
|
||||
1. **User Registration & Authentication**
|
||||
- Email/password registration
|
||||
- JWT-based session management
|
||||
- Password hashing with bcrypt
|
||||
|
||||
2. **Download Tracking**
|
||||
- IP-based tracking (1 free download)
|
||||
- Unlimited downloads for members
|
||||
- Automatic tracking on download clicks
|
||||
|
||||
3. **Paywall System**
|
||||
- Library downloads (1 free, then paywall)
|
||||
- Contact form (members only)
|
||||
- Email address (members only)
|
||||
- Testimonials page (members only)
|
||||
|
||||
4. **Admin Panel**
|
||||
- Secure admin login
|
||||
- Approve/deny memberships
|
||||
- Moderate testimonials
|
||||
- View pending members
|
||||
|
||||
5. **Email Notifications**
|
||||
- Signup notifications to admin
|
||||
- Approval confirmations to users
|
||||
- Contact form submissions
|
||||
|
||||
6. **Buy Me a Coffee Integration**
|
||||
- Prominent $5 donation button
|
||||
- Users donate first, then sign up
|
||||
- Manual verification by admin
|
||||
|
||||
## Configuration Required
|
||||
|
||||
1. **Environment Variables** (set before running API):
|
||||
```
|
||||
ADMIN_PASSWORD=your-strong-password
|
||||
ADMIN_EMAIL=indiana.holmes8@gmail.com
|
||||
JWT_SECRET=random-secret-key
|
||||
MEMBERSHIP_PORT=5001
|
||||
SMTP_HOST=smtp.gmail.com (optional)
|
||||
SMTP_PORT=587 (optional)
|
||||
SMTP_USER=your-email@gmail.com (optional)
|
||||
SMTP_PASS=your-app-password (optional)
|
||||
```
|
||||
|
||||
2. **API URL Configuration** (update for production):
|
||||
- `static/js/membership.js` line 4: `API_BASE`
|
||||
- `layouts/admin/single.html` line 186: `ADMIN_API_BASE`
|
||||
|
||||
3. **Start the API Server**:
|
||||
```bash
|
||||
python3 /root/hydro-sterile/scripts/membership_api.py
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
SQLite database automatically created at:
|
||||
`/root/hydro-sterile/data/members.db`
|
||||
|
||||
Tables:
|
||||
- `users` - User accounts and status
|
||||
- `downloads` - Download tracking
|
||||
- `testimonials` - User testimonials
|
||||
- `sessions` - User sessions
|
||||
- `admin_sessions` - Admin sessions
|
||||
- `contact_submissions` - Contact form submissions
|
||||
|
||||
## Security Features
|
||||
|
||||
- Password hashing (bcrypt)
|
||||
- JWT tokens with expiration
|
||||
- Rate limiting on all endpoints
|
||||
- Input sanitization
|
||||
- SQL injection prevention (parameterized queries)
|
||||
- CSRF protection ready
|
||||
- Secure admin authentication
|
||||
- Email privacy (not exposed in frontend)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Set environment variables
|
||||
2. Install dependencies: `pip3 install -r requirements.txt`
|
||||
3. Start API server: `python3 scripts/membership_api.py`
|
||||
4. Test registration flow
|
||||
5. Test admin panel
|
||||
6. Update API URLs for production
|
||||
7. Configure SMTP for email notifications
|
||||
8. Test email sending
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] User can register after donation
|
||||
- [ ] Admin receives signup notification
|
||||
- [ ] Admin can approve/deny members
|
||||
- [ ] Approved users receive confirmation email
|
||||
- [ ] Users can log in
|
||||
- [ ] Download tracking works (IP-based)
|
||||
- [ ] Paywall shows after 1 download
|
||||
- [ ] Members get unlimited downloads
|
||||
- [ ] Contact form only works for members
|
||||
- [ ] Email address hidden for non-members
|
||||
- [ ] Testimonials page paywalled
|
||||
- [ ] Members can submit testimonials
|
||||
- [ ] Admin can moderate testimonials
|
||||
|
||||
122
MEMBERSHIP_SETUP.md
Normal file
122
MEMBERSHIP_SETUP.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Membership System Setup Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This membership system provides:
|
||||
- User registration and authentication
|
||||
- IP-based download limiting (1 free download, unlimited for members)
|
||||
- Paywall for library downloads, contact form, and testimonials
|
||||
- Admin panel for membership approval
|
||||
- Buy Me a Coffee integration ($5 minimum donation)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Python 3.7+ with pip
|
||||
2. Flask and other dependencies (see requirements.txt)
|
||||
3. SMTP server access for email notifications (optional but recommended)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install Python dependencies:
|
||||
```bash
|
||||
cd /root/hydro-sterile
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Set environment variables:
|
||||
```bash
|
||||
export ADMIN_PASSWORD="your-very-secure-password-here"
|
||||
export ADMIN_EMAIL="indiana.holmes8@gmail.com"
|
||||
export JWT_SECRET="$(openssl rand -hex 32)"
|
||||
export MEMBERSHIP_PORT=5001
|
||||
|
||||
# Optional: SMTP configuration for email notifications
|
||||
export SMTP_HOST="smtp.gmail.com"
|
||||
export SMTP_PORT=587
|
||||
export SMTP_USER="your-email@gmail.com"
|
||||
export SMTP_PASS="your-app-password"
|
||||
```
|
||||
|
||||
3. Create a systemd service or run manually:
|
||||
```bash
|
||||
python3 /root/hydro-sterile/scripts/membership_api.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The membership API runs on port 5001 by default. Update the API_BASE URL in:
|
||||
- `/root/hydro-sterile/static/js/membership.js` (line 4)
|
||||
- `/root/hydro-sterile/layouts/admin/single.html` (line 186)
|
||||
|
||||
For production, change from `http://10.30.20.243:5001` to your production domain.
|
||||
|
||||
### Admin Panel
|
||||
|
||||
Access the admin panel at `/admin/` on your Hugo site.
|
||||
|
||||
The admin password is set via the `ADMIN_PASSWORD` environment variable. Use a strong password!
|
||||
|
||||
### Database
|
||||
|
||||
The SQLite database is automatically created at:
|
||||
`/root/hydro-sterile/data/members.db`
|
||||
|
||||
Backup this file regularly!
|
||||
|
||||
## User Flow
|
||||
|
||||
1. User visits paywalled page (library, contact, testimonials)
|
||||
2. Non-members see paywall message directing to membership page
|
||||
3. User completes Buy Me a Coffee donation ($5 minimum)
|
||||
4. User signs up with email/password on membership page
|
||||
5. System sends email notification to admin
|
||||
6. Admin logs into admin panel and approves/denies membership
|
||||
7. Approved user receives confirmation email
|
||||
8. User can now log in and access all paywalled content
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Admin password should be very strong (20+ characters recommended)
|
||||
- JWT_SECRET should be a random string (use `openssl rand -hex 32`)
|
||||
- Never commit ADMIN_PASSWORD or JWT_SECRET to version control
|
||||
- Database file should have restricted permissions (600)
|
||||
- SMTP credentials should use app passwords, not main account password
|
||||
|
||||
## Email Configuration
|
||||
|
||||
For Gmail:
|
||||
1. Enable 2-factor authentication
|
||||
2. Generate an app password
|
||||
3. Use the app password as SMTP_PASS
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API not starting
|
||||
- Check port 5001 is not in use: `netstat -tuln | grep 5001`
|
||||
- Check Python dependencies: `pip3 list | grep -E "flask|bcrypt|jwt"`
|
||||
|
||||
### Database errors
|
||||
- Ensure `/root/hydro-sterile/data/` directory exists and is writable
|
||||
- Check file permissions: `chmod 600 /root/hydro-sterile/data/members.db`
|
||||
|
||||
### Email not sending
|
||||
- Check SMTP credentials
|
||||
- Verify SMTP_HOST and SMTP_PORT are correct
|
||||
- Check firewall allows outbound SMTP connections
|
||||
- Email sending failures won't break the system, but admin won't get notifications
|
||||
|
||||
### Frontend not connecting to API
|
||||
- Check API_BASE URL in membership.js matches your server
|
||||
- Check CORS settings in membership_api.py
|
||||
- Check browser console for errors
|
||||
- Verify API is running: `curl http://localhost:5001/api/health`
|
||||
|
||||
## Maintenance
|
||||
|
||||
- Regularly backup `/root/hydro-sterile/data/members.db`
|
||||
- Monitor API logs for errors
|
||||
- Review pending memberships regularly
|
||||
- Clean up old sessions periodically (optional, sessions expire automatically)
|
||||
|
||||
29
README.md
Normal file
29
README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Pure Hydroponics Site
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Install Dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Run Development Server:**
|
||||
```bash
|
||||
# Uses local hugo binary via hugo-bin
|
||||
./node_modules/.bin/hugo server
|
||||
```
|
||||
|
||||
3. **Build for Production:**
|
||||
```bash
|
||||
./node_modules/.bin/hugo --minify
|
||||
```
|
||||
|
||||
## Stack
|
||||
- **Hugo Extended** (v0.152+)
|
||||
- **Tailwind CSS** (v4.0) via PostCSS
|
||||
- **Alpine.js** for interactions
|
||||
|
||||
## Deployment
|
||||
- **Output Directory:** `public/`
|
||||
- **Cloudflare Pages:** Set build command to `npm run build` (add script to package.json) or just run hugo.
|
||||
- **Netlify:** Similar setup.
|
||||
36
RECOVERY.md
Normal file
36
RECOVERY.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Website Service Management Guide
|
||||
|
||||
This document lists the services that run the website and how to manage them.
|
||||
|
||||
## Services
|
||||
|
||||
| Service Name | Description | Port |
|
||||
|--------------|-------------|------|
|
||||
| `hugo-hydrolord.service` | Main Hugo Website | 7575 |
|
||||
| `hugo-membership-api.service` | Membership & Static API | 5001 |
|
||||
| `hugo-chat-api.service` | AI Chat API | 5000 |
|
||||
| `hugo-blog-generator.timer` | Auto Blog Generator | N/A |
|
||||
|
||||
## Management Commands
|
||||
|
||||
### Check Status
|
||||
```bash
|
||||
systemctl status \"hugo-*\"
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# Website logs
|
||||
journalctl -u hugo-hydrolord.service -f
|
||||
|
||||
# API logs
|
||||
journalctl -u hugo-membership-api.service -f
|
||||
```
|
||||
|
||||
### Restart All
|
||||
```bash
|
||||
systemctl restart hugo-hydrolord.service hugo-membership-api.service hugo-chat-api.service
|
||||
```
|
||||
|
||||
## Persistence
|
||||
All services are set to `Restart=always` and are `enabled` to start at boot automatically.
|
||||
5
archetypes/default.md
Normal file
5
archetypes/default.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: '{{ .Date }}'
|
||||
draft: true
|
||||
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
---
|
||||
21
assets/css/main.css
Normal file
21
assets/css/main.css
Normal file
@@ -0,0 +1,21 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary: #059669;
|
||||
--color-secondary: #0ea5e9;
|
||||
--color-surface: #1f2937;
|
||||
--color-background: #111827;
|
||||
--font-sans: 'Inter', 'system-ui', 'sans-serif';
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
color: #e5e7eb;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 700;
|
||||
color: #f9fafb;
|
||||
}
|
||||
}
|
||||
40
content/about.md
Normal file
40
content/about.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "About Me"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
description: "Sterile hydro nerd with 20 years of dialing in award-winning cannabis."
|
||||
---
|
||||
|
||||
# About Me
|
||||
|
||||
I'm 35, 20 years deep growing legal weed, and I'm a laid-back stoner who treats hydro like a lab. Every photo here is cannabis because that's my playground—clean water, clean salts, no funk.
|
||||
|
||||
Most growers swear you need microbes and that root rot is just part of the game.
|
||||
|
||||
**They're wrong.**
|
||||
|
||||
I've spent two decades perfecting something most people think is impossible: **completely sterile hydroponics**. No bacteria. No "beneficials." No pathogens. Just roots ripping through clean solution.
|
||||
|
||||
Stop listening to randoms on Reddit. You won't find this on YouTube or in standard grow guides. What I teach are the protocols that make buds so clean people ask if they're even real.
|
||||
|
||||
## My Philosophy: No Excuses Hydro or Go Home
|
||||
|
||||
**No excuses hydro or go home.** Precision beats fairy tales. If something drifts, we fix the variable and keep moving.
|
||||
|
||||
Hydroponics isn't about excuses. It's about control—eliminating every variable that can go wrong so your plants hit their genetic ceiling.
|
||||
|
||||
My background is in clinical microbiology. In the lab, contamination equals failure. I pulled that same mindset into growing and watched plants thrive once every biological hitchhiker was gone.
|
||||
|
||||
The methods I use come from medical sterility protocols. They aren't theories. They're battle-tested and tuned over thousands of hours.
|
||||
|
||||
## Two Decades of Results
|
||||
|
||||
Over the last 20 years, I've helped small home setups, basement grows, and big commercial rooms spanning thousands of square feet. Large gardens, small gardens—I dial them all in.
|
||||
|
||||
I've watched basement growers ditch slime and harvest jars that make commercial facilities jealous. I've seen large operations eliminate whole categories of problems they thought were "just part of the business" and start pulling competition-grade flower that sells itself.
|
||||
|
||||
When commercial operations need results, they call me. When home growers want to level up, they call me.
|
||||
|
||||
If you follow the sterile protocols I've honed over the past two decades, you **will** see the difference: no root rot, faster growth, bigger yields, clean white roots, and plants that aren't fighting anything but the trellis.
|
||||
|
||||
No excuses. Award-winning results. Ready?
|
||||
7
content/admin.md
Normal file
7
content/admin.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Admin Panel"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
layout: "admin"
|
||||
---
|
||||
|
||||
4
content/albums/_index.md
Normal file
4
content/albums/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Photo Albums"
|
||||
description: "A visual journey through sterile hydroponic systems."
|
||||
---
|
||||
30
content/albums/commercial-lettuce.md
Normal file
30
content/albums/commercial-lettuce.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Commercial NFT Lettuce"
|
||||
description: "A 2000 sq ft facility designed for zero-waste production."
|
||||
cover: "/images/IMG_2871.jpeg"
|
||||
images:
|
||||
- url: "/images/IMG_9463.jpeg"
|
||||
caption: "IMG 9463.jpeg"
|
||||
- url: "/images/69844719615__6435DF53-250E-4D10-B453-4DBE2ECC44F1.jpeg"
|
||||
caption: "69844719615 6435DF53 250E 4D10 B453 4DBE2ECC44F1.jpeg"
|
||||
- url: "/images/IMG_0081.jpeg"
|
||||
caption: "IMG 0081.jpeg"
|
||||
- url: "/images/IMG_9313.jpeg"
|
||||
caption: "IMG 9313.jpeg"
|
||||
- url: "/images/IMG_8923.jpeg"
|
||||
caption: "IMG 8923.jpeg"
|
||||
- url: "/images/IMG_8405.jpeg"
|
||||
caption: "IMG 8405.jpeg"
|
||||
- url: "/images/IMG_9198.jpeg"
|
||||
caption: "IMG 9198.jpeg"
|
||||
- url: "/images/IMG_1859.jpeg"
|
||||
caption: "IMG 1859.jpeg"
|
||||
- url: "/images/IMG_1436.jpeg"
|
||||
caption: "IMG 1436.jpeg"
|
||||
- url: "/images/433320B3-7D36-4965-9E58-DEC07BE6827C.jpg"
|
||||
caption: "433320B3 7D36 4965 9E58 DEC07BE6827C.jpg"
|
||||
- url: "/images/IMG_1837.jpeg"
|
||||
caption: "IMG 1837.jpeg"
|
||||
- url: "/images/IMG_8894.jpeg"
|
||||
caption: "IMG 8894.jpeg"
|
||||
---
|
||||
30
content/albums/medical-dwc.md
Normal file
30
content/albums/medical-dwc.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Medical RDWC Build"
|
||||
description: "High-precision Recirculating Deep Water Culture for medicinal crops."
|
||||
cover: "/images/IMG_0165 2.jpeg"
|
||||
images:
|
||||
- url: "/images/IMG_1439.jpeg"
|
||||
caption: "IMG 1439.jpeg"
|
||||
- url: "/images/IMG_1436.jpeg"
|
||||
caption: "IMG 1436.jpeg"
|
||||
- url: "/images/IMG_0081.jpeg"
|
||||
caption: "IMG 0081.jpeg"
|
||||
- url: "/images/IMG_1534.jpeg"
|
||||
caption: "IMG 1534.jpeg"
|
||||
- url: "/images/IMG_8377.jpeg"
|
||||
caption: "IMG 8377.jpeg"
|
||||
- url: "/images/IMG_1126.jpeg"
|
||||
caption: "IMG 1126.jpeg"
|
||||
- url: "/images/IMG_7684.jpeg"
|
||||
caption: "IMG 7684.jpeg"
|
||||
- url: "/images/IMG_2838.jpeg"
|
||||
caption: "IMG 2838.jpeg"
|
||||
- url: "/images/IMG_9328 3.jpeg"
|
||||
caption: "IMG 9328 3.jpeg"
|
||||
- url: "/images/IMG_9328.jpeg"
|
||||
caption: "IMG 9328.jpeg"
|
||||
- url: "/images/IMG_6475.jpeg"
|
||||
caption: "IMG 6475.jpeg"
|
||||
- url: "/images/E142B0A8-8F28-4288-9B10-770518A3C43B.webp"
|
||||
caption: "E142B0A8 8F28 4288 9B10 770518A3C43B.webp"
|
||||
---
|
||||
338
content/albums/my-garden.md
Normal file
338
content/albums/my-garden.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
title: "My Hydroponic Garden"
|
||||
date: 2024-03-25T12:00:00-05:00
|
||||
description: "A comprehensive gallery of my hydroponic plants, setups, and results."
|
||||
images:
|
||||
- url: "/images/13AC76BC-B45E-42F1-A9C3-A9E961B3C496.webp"
|
||||
caption: "13AC76BC B45E 42F1 A9C3 A9E961B3C496.webp"
|
||||
- url: "/images/20012743-FFB5-4AFB-9DE6-5BE122236EA8.webp"
|
||||
caption: "20012743 FFB5 4AFB 9DE6 5BE122236EA8.webp"
|
||||
- url: "/images/433320B3-7D36-4965-9E58-DEC07BE6827C.jpg"
|
||||
caption: "433320B3 7D36 4965 9E58 DEC07BE6827C.jpg"
|
||||
- url: "/images/54d957ebf6c9447c8c3214fe371c303a.MOV"
|
||||
caption: "54d957ebf6c9447c8c3214fe371c303a.MOV"
|
||||
- url: "/images/64634090387__02F28F99-0128-4628-9F5A-AA3BF5043B30.jpeg"
|
||||
caption: "64634090387 02F28F99 0128 4628 9F5A AA3BF5043B30.jpeg"
|
||||
- url: "/images/64792744957__B54FD5A8-4237-4141-8090-E2BED845456C.jpeg"
|
||||
caption: "64792744957 B54FD5A8 4237 4141 8090 E2BED845456C.jpeg"
|
||||
- url: "/images/65331164721__D4F8AED9-9B4A-4E75-905D-5027A2D8E440.jpeg"
|
||||
caption: "65331164721 D4F8AED9 9B4A 4E75 905D 5027A2D8E440.jpeg"
|
||||
- url: "/images/65804214832__CCBE50A0-8F17-4B36-817F-D1FBBA1A4B3F.jpeg"
|
||||
caption: "65804214832 CCBE50A0 8F17 4B36 817F D1FBBA1A4B3F.jpeg"
|
||||
- url: "/images/65804215588__E8E13980-3188-4726-AFE4-0C038A6422C8.jpeg"
|
||||
caption: "65804215588 E8E13980 3188 4726 AFE4 0C038A6422C8.jpeg"
|
||||
- url: "/images/69844719615__6435DF53-250E-4D10-B453-4DBE2ECC44F1.jpeg"
|
||||
caption: "69844719615 6435DF53 250E 4D10 B453 4DBE2ECC44F1.jpeg"
|
||||
- url: "/images/69844720921__44164E36-77D1-4DBA-BD06-26F8BF86C541.jpeg"
|
||||
caption: "69844720921 44164E36 77D1 4DBA BD06 26F8BF86C541.jpeg"
|
||||
- url: "/images/707F7B39-B98D-44CC-AE45-9EF2126F5887.webp"
|
||||
caption: "707F7B39 B98D 44CC AE45 9EF2126F5887.webp"
|
||||
- url: "/images/7D196C8E-A924-4600-922D-52FDB61C41D2.png"
|
||||
caption: "7D196C8E A924 4600 922D 52FDB61C41D2.png"
|
||||
- url: "/images/BF9E2340-832A-49CD-BED3-4A1801C35E97.jpg"
|
||||
caption: "BF9E2340 832A 49CD BED3 4A1801C35E97.jpg"
|
||||
- url: "/images/BF9F1085-AB99-47FA-97E4-56B5658E42FE.webp"
|
||||
caption: "BF9F1085 AB99 47FA 97E4 56B5658E42FE.webp"
|
||||
- url: "/images/C56C173F-1C51-4F3D-9E86-AA24B06EEC19.webp"
|
||||
caption: "C56C173F 1C51 4F3D 9E86 AA24B06EEC19.webp"
|
||||
- url: "/images/ComfyUI_00012_.png"
|
||||
caption: "ComfyUI 00012 .png"
|
||||
- url: "/images/ComfyUI_00013_.png"
|
||||
caption: "ComfyUI 00013 .png"
|
||||
- url: "/images/ComfyUI_00014_.png"
|
||||
caption: "ComfyUI 00014 .png"
|
||||
- url: "/images/ComfyUI_00015_.png"
|
||||
caption: "ComfyUI 00015 .png"
|
||||
- url: "/images/ComfyUI_00023_.png"
|
||||
caption: "ComfyUI 00023 .png"
|
||||
- url: "/images/D57F541F-52FA-4B45-9716-758F67F55D79.webp"
|
||||
caption: "D57F541F 52FA 4B45 9716 758F67F55D79.webp"
|
||||
- url: "/images/E142B0A8-8F28-4288-9B10-770518A3C43B.webp"
|
||||
caption: "E142B0A8 8F28 4288 9B10 770518A3C43B.webp"
|
||||
- url: "/images/E8211B06-A9E4-4A51-A941-757CE191BC57.jpg"
|
||||
caption: "E8211B06 A9E4 4A51 A941 757CE191BC57.jpg"
|
||||
- url: "/images/E9DFB924-05F8-4079-9AFD-269567B514D8.jpg"
|
||||
caption: "E9DFB924 05F8 4079 9AFD 269567B514D8.jpg"
|
||||
- url: "/images/IMG_0009.jpeg"
|
||||
caption: "IMG 0009.jpeg"
|
||||
- url: "/images/IMG_0081.jpeg"
|
||||
caption: "IMG 0081.jpeg"
|
||||
- url: "/images/IMG_0113.jpeg"
|
||||
caption: "IMG 0113.jpeg"
|
||||
- url: "/images/IMG_0165 2.jpeg"
|
||||
caption: "IMG 0165 2.jpeg"
|
||||
- url: "/images/IMG_0165.jpeg"
|
||||
caption: "IMG 0165.jpeg"
|
||||
- url: "/images/IMG_0172.jpeg"
|
||||
caption: "IMG 0172.jpeg"
|
||||
- url: "/images/IMG_0179 2.jpeg"
|
||||
caption: "IMG 0179 2.jpeg"
|
||||
- url: "/images/IMG_0179 3.jpeg"
|
||||
caption: "IMG 0179 3.jpeg"
|
||||
- url: "/images/IMG_0179 4.jpeg"
|
||||
caption: "IMG 0179 4.jpeg"
|
||||
- url: "/images/IMG_0179.jpeg"
|
||||
caption: "IMG 0179.jpeg"
|
||||
- url: "/images/IMG_0332.jpeg"
|
||||
caption: "IMG 0332.jpeg"
|
||||
- url: "/images/IMG_0333.jpeg"
|
||||
caption: "IMG 0333.jpeg"
|
||||
- url: "/images/IMG_0334.jpeg"
|
||||
caption: "IMG 0334.jpeg"
|
||||
- url: "/images/IMG_0599.jpeg"
|
||||
caption: "IMG 0599.jpeg"
|
||||
- url: "/images/IMG_0784.jpeg"
|
||||
caption: "IMG 0784.jpeg"
|
||||
- url: "/images/IMG_0801.jpeg"
|
||||
caption: "IMG 0801.jpeg"
|
||||
- url: "/images/IMG_0904.jpeg"
|
||||
caption: "IMG 0904.jpeg"
|
||||
- url: "/images/IMG_0CFF7F7D-C8F8-4051-BCA5-B98C40D9D387.jpeg"
|
||||
caption: "IMG 0CFF7F7D C8F8 4051 BCA5 B98C40D9D387.jpeg"
|
||||
- url: "/images/IMG_1011 2.HEIC"
|
||||
caption: "IMG 1011 2.HEIC"
|
||||
- url: "/images/IMG_1011.HEIC"
|
||||
caption: "IMG 1011.HEIC"
|
||||
- url: "/images/IMG_1020 2.jpeg"
|
||||
caption: "IMG 1020 2.jpeg"
|
||||
- url: "/images/IMG_1020.jpeg"
|
||||
caption: "IMG 1020.jpeg"
|
||||
- url: "/images/IMG_1021.jpeg"
|
||||
caption: "IMG 1021.jpeg"
|
||||
- url: "/images/IMG_1058.jpeg"
|
||||
caption: "IMG 1058.jpeg"
|
||||
- url: "/images/IMG_1062 2.jpeg"
|
||||
caption: "IMG 1062 2.jpeg"
|
||||
- url: "/images/IMG_1062.jpeg"
|
||||
caption: "IMG 1062.jpeg"
|
||||
- url: "/images/IMG_1104 2.jpeg"
|
||||
caption: "IMG 1104 2.jpeg"
|
||||
- url: "/images/IMG_1104.jpeg"
|
||||
caption: "IMG 1104.jpeg"
|
||||
- url: "/images/IMG_1126.jpeg"
|
||||
caption: "IMG 1126.jpeg"
|
||||
- url: "/images/IMG_1160.jpeg"
|
||||
caption: "IMG 1160.jpeg"
|
||||
- url: "/images/IMG_1435.jpeg"
|
||||
caption: "IMG 1435.jpeg"
|
||||
- url: "/images/IMG_1436.jpeg"
|
||||
caption: "IMG 1436.jpeg"
|
||||
- url: "/images/IMG_1439.jpeg"
|
||||
caption: "IMG 1439.jpeg"
|
||||
- url: "/images/IMG_1534.jpeg"
|
||||
caption: "IMG 1534.jpeg"
|
||||
- url: "/images/IMG_1767.jpeg"
|
||||
caption: "IMG 1767.jpeg"
|
||||
- url: "/images/IMG_1768.jpeg"
|
||||
caption: "IMG 1768.jpeg"
|
||||
- url: "/images/IMG_1837.jpeg"
|
||||
caption: "IMG 1837.jpeg"
|
||||
- url: "/images/IMG_1859.jpeg"
|
||||
caption: "IMG 1859.jpeg"
|
||||
- url: "/images/IMG_1864.jpeg"
|
||||
caption: "IMG 1864.jpeg"
|
||||
- url: "/images/IMG_2822.jpeg"
|
||||
caption: "IMG 2822.jpeg"
|
||||
- url: "/images/IMG_2826.jpeg"
|
||||
caption: "IMG 2826.jpeg"
|
||||
- url: "/images/IMG_2828.jpeg"
|
||||
caption: "IMG 2828.jpeg"
|
||||
- url: "/images/IMG_2835.jpeg"
|
||||
caption: "IMG 2835.jpeg"
|
||||
- url: "/images/IMG_2838.jpeg"
|
||||
caption: "IMG 2838.jpeg"
|
||||
- url: "/images/IMG_2870.jpeg"
|
||||
caption: "IMG 2870.jpeg"
|
||||
- url: "/images/IMG_2871.jpeg"
|
||||
caption: "IMG 2871.jpeg"
|
||||
- url: "/images/IMG_3528.jpeg"
|
||||
caption: "IMG 3528.jpeg"
|
||||
- url: "/images/IMG_4029.jpeg"
|
||||
caption: "IMG 4029.jpeg"
|
||||
- url: "/images/IMG_4674.JPG"
|
||||
caption: "IMG 4674.JPG"
|
||||
- url: "/images/IMG_5008.jpeg"
|
||||
caption: "IMG 5008.jpeg"
|
||||
- url: "/images/IMG_5218.jpeg"
|
||||
caption: "IMG 5218.jpeg"
|
||||
- url: "/images/IMG_5261.jpeg"
|
||||
caption: "IMG 5261.jpeg"
|
||||
- url: "/images/IMG_5262.jpeg"
|
||||
caption: "IMG 5262.jpeg"
|
||||
- url: "/images/IMG_5374.jpeg"
|
||||
caption: "IMG 5374.jpeg"
|
||||
- url: "/images/IMG_5698.jpeg"
|
||||
caption: "IMG 5698.jpeg"
|
||||
- url: "/images/IMG_6108.jpeg"
|
||||
caption: "IMG 6108.jpeg"
|
||||
- url: "/images/IMG_6180.jpeg"
|
||||
caption: "IMG 6180.jpeg"
|
||||
- url: "/images/IMG_6389.jpeg"
|
||||
caption: "IMG 6389.jpeg"
|
||||
- url: "/images/IMG_6426.jpeg"
|
||||
caption: "IMG 6426.jpeg"
|
||||
- url: "/images/IMG_6475.jpeg"
|
||||
caption: "IMG 6475.jpeg"
|
||||
- url: "/images/IMG_6477.jpeg"
|
||||
caption: "IMG 6477.jpeg"
|
||||
- url: "/images/IMG_6501.jpeg"
|
||||
caption: "IMG 6501.jpeg"
|
||||
- url: "/images/IMG_6574.jpeg"
|
||||
caption: "IMG 6574.jpeg"
|
||||
- url: "/images/IMG_6919.jpeg"
|
||||
caption: "IMG 6919.jpeg"
|
||||
- url: "/images/IMG_6920.jpeg"
|
||||
caption: "IMG 6920.jpeg"
|
||||
- url: "/images/IMG_7077.jpeg"
|
||||
caption: "IMG 7077.jpeg"
|
||||
- url: "/images/IMG_7078.jpeg"
|
||||
caption: "IMG 7078.jpeg"
|
||||
- url: "/images/IMG_7159.jpeg"
|
||||
caption: "IMG 7159.jpeg"
|
||||
- url: "/images/IMG_7374.jpeg"
|
||||
caption: "IMG 7374.jpeg"
|
||||
- url: "/images/IMG_7447.jpeg"
|
||||
caption: "IMG 7447.jpeg"
|
||||
- url: "/images/IMG_7677.jpeg"
|
||||
caption: "IMG 7677.jpeg"
|
||||
- url: "/images/IMG_7684.jpeg"
|
||||
caption: "IMG 7684.jpeg"
|
||||
- url: "/images/IMG_7712.JPG"
|
||||
caption: "IMG 7712.JPG"
|
||||
- url: "/images/IMG_7999.jpeg"
|
||||
caption: "IMG 7999.jpeg"
|
||||
- url: "/images/IMG_8093.jpeg"
|
||||
caption: "IMG 8093.jpeg"
|
||||
- url: "/images/IMG_8095.jpeg"
|
||||
caption: "IMG 8095.jpeg"
|
||||
- url: "/images/IMG_8377.jpeg"
|
||||
caption: "IMG 8377.jpeg"
|
||||
- url: "/images/IMG_8405.jpeg"
|
||||
caption: "IMG 8405.jpeg"
|
||||
- url: "/images/IMG_8483.jpeg"
|
||||
caption: "IMG 8483.jpeg"
|
||||
- url: "/images/IMG_8586.jpeg"
|
||||
caption: "IMG 8586.jpeg"
|
||||
- url: "/images/IMG_8594.jpeg"
|
||||
caption: "IMG 8594.jpeg"
|
||||
- url: "/images/IMG_8894.jpeg"
|
||||
caption: "IMG 8894.jpeg"
|
||||
- url: "/images/IMG_8923.jpeg"
|
||||
caption: "IMG 8923.jpeg"
|
||||
- url: "/images/IMG_8926.jpeg"
|
||||
caption: "IMG 8926.jpeg"
|
||||
- url: "/images/IMG_9103.jpeg"
|
||||
caption: "IMG 9103.jpeg"
|
||||
- url: "/images/IMG_9104.jpeg"
|
||||
caption: "IMG 9104.jpeg"
|
||||
- url: "/images/IMG_9112.jpeg"
|
||||
caption: "IMG 9112.jpeg"
|
||||
- url: "/images/IMG_9129.jpeg"
|
||||
caption: "IMG 9129.jpeg"
|
||||
- url: "/images/IMG_9135.jpeg"
|
||||
caption: "IMG 9135.jpeg"
|
||||
- url: "/images/IMG_9166.jpeg"
|
||||
caption: "IMG 9166.jpeg"
|
||||
- url: "/images/IMG_9167.jpeg"
|
||||
caption: "IMG 9167.jpeg"
|
||||
- url: "/images/IMG_9195.jpeg"
|
||||
caption: "IMG 9195.jpeg"
|
||||
- url: "/images/IMG_9196 2.jpeg"
|
||||
caption: "IMG 9196 2.jpeg"
|
||||
- url: "/images/IMG_9196 3.jpeg"
|
||||
caption: "IMG 9196 3.jpeg"
|
||||
- url: "/images/IMG_9196 4.jpeg"
|
||||
caption: "IMG 9196 4.jpeg"
|
||||
- url: "/images/IMG_9196.jpeg"
|
||||
caption: "IMG 9196.jpeg"
|
||||
- url: "/images/IMG_9198.jpeg"
|
||||
caption: "IMG 9198.jpeg"
|
||||
- url: "/images/IMG_9202.MOV"
|
||||
caption: "IMG 9202.MOV"
|
||||
- url: "/images/IMG_9242.jpeg"
|
||||
caption: "IMG 9242.jpeg"
|
||||
- url: "/images/IMG_9255.jpeg"
|
||||
caption: "IMG 9255.jpeg"
|
||||
- url: "/images/IMG_9269.jpeg"
|
||||
caption: "IMG 9269.jpeg"
|
||||
- url: "/images/IMG_9308.jpeg"
|
||||
caption: "IMG 9308.jpeg"
|
||||
- url: "/images/IMG_9313.jpeg"
|
||||
caption: "IMG 9313.jpeg"
|
||||
- url: "/images/IMG_9316.jpeg"
|
||||
caption: "IMG 9316.jpeg"
|
||||
- url: "/images/IMG_9328 2.jpeg"
|
||||
caption: "IMG 9328 2.jpeg"
|
||||
- url: "/images/IMG_9328 3.jpeg"
|
||||
caption: "IMG 9328 3.jpeg"
|
||||
- url: "/images/IMG_9328.jpeg"
|
||||
caption: "IMG 9328.jpeg"
|
||||
- url: "/images/IMG_9335.jpeg"
|
||||
caption: "IMG 9335.jpeg"
|
||||
- url: "/images/IMG_9357.PNG"
|
||||
caption: "IMG 9357.PNG"
|
||||
- url: "/images/IMG_9367.jpeg"
|
||||
caption: "IMG 9367.jpeg"
|
||||
- url: "/images/IMG_9385.jpeg"
|
||||
caption: "IMG 9385.jpeg"
|
||||
- url: "/images/IMG_9410.jpeg"
|
||||
caption: "IMG 9410.jpeg"
|
||||
- url: "/images/IMG_9411.jpeg"
|
||||
caption: "IMG 9411.jpeg"
|
||||
- url: "/images/IMG_9413.jpeg"
|
||||
caption: "IMG 9413.jpeg"
|
||||
- url: "/images/IMG_9419.jpeg"
|
||||
caption: "IMG 9419.jpeg"
|
||||
- url: "/images/IMG_9428.jpeg"
|
||||
caption: "IMG 9428.jpeg"
|
||||
- url: "/images/IMG_9429.jpeg"
|
||||
caption: "IMG 9429.jpeg"
|
||||
- url: "/images/IMG_9432.jpeg"
|
||||
caption: "IMG 9432.jpeg"
|
||||
- url: "/images/IMG_9446.jpeg"
|
||||
caption: "IMG 9446.jpeg"
|
||||
- url: "/images/IMG_9448.jpeg"
|
||||
caption: "IMG 9448.jpeg"
|
||||
- url: "/images/IMG_9449.jpeg"
|
||||
caption: "IMG 9449.jpeg"
|
||||
- url: "/images/IMG_9454.jpeg"
|
||||
caption: "IMG 9454.jpeg"
|
||||
- url: "/images/IMG_9463.jpeg"
|
||||
caption: "IMG 9463.jpeg"
|
||||
- url: "/images/IMG_9519.jpeg"
|
||||
caption: "IMG 9519.jpeg"
|
||||
- url: "/images/IMG_9520.jpeg"
|
||||
caption: "IMG 9520.jpeg"
|
||||
- url: "/images/IMG_9564.jpeg"
|
||||
caption: "IMG 9564.jpeg"
|
||||
- url: "/images/IMG_9612 2.jpeg"
|
||||
caption: "IMG 9612 2.jpeg"
|
||||
- url: "/images/IMG_9612.jpeg"
|
||||
caption: "IMG 9612.jpeg"
|
||||
- url: "/images/IMG_9613 2.jpeg"
|
||||
caption: "IMG 9613 2.jpeg"
|
||||
- url: "/images/IMG_9613 3.jpeg"
|
||||
caption: "IMG 9613 3.jpeg"
|
||||
- url: "/images/IMG_9613.jpeg"
|
||||
caption: "IMG 9613.jpeg"
|
||||
- url: "/images/IMG_9768.jpeg"
|
||||
caption: "IMG 9768.jpeg"
|
||||
- url: "/images/IMG_9769.jpeg"
|
||||
caption: "IMG 9769.jpeg"
|
||||
- url: "/images/Screenshot 2024-03-25 195708.png"
|
||||
caption: "Screenshot 2024 03 25 195708.png"
|
||||
- url: "/images/WBYS8305.JPEG"
|
||||
caption: "WBYS8305.JPEG"
|
||||
- url: "/images/f130260032.jpeg"
|
||||
caption: "f130260032.jpeg"
|
||||
- url: "/images/f130303808.jpeg"
|
||||
caption: "f130303808.jpeg"
|
||||
- url: "/images/f131874624.jpeg"
|
||||
caption: "f131874624.jpeg"
|
||||
- url: "/images/f139364160.jpeg"
|
||||
caption: "f139364160.jpeg"
|
||||
- url: "/images/f153047488.jpeg"
|
||||
caption: "f153047488.jpeg"
|
||||
- url: "/images/f157200512.jpeg"
|
||||
caption: "f157200512.jpeg"
|
||||
---
|
||||
4
content/blog/_index.md
Normal file
4
content/blog/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Grow Journal"
|
||||
description: "Notes from the field."
|
||||
---
|
||||
12
content/blog/post-1.md
Normal file
12
content/blog/post-1.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "Why I Stopped Using Organic Nutrients in Hydro"
|
||||
date: 2024-01-10T10:00:00Z
|
||||
image: "/images/69844720921__44164E36-77D1-4DBA-BD06-26F8BF86C541.jpeg"
|
||||
summary: "Biofilm, clogs, and unstable pH. Why organics belong in soil, not in your reservoir."
|
||||
---
|
||||
|
||||
I wanted to believe in "organic hydroponics." Clean water plus natural nutrients sounded dreamy. After three straight failures, I had to face the music.
|
||||
|
||||
Organic nutrients are liquid compost. In soil, microbes break them down. In a sterile reservoir they just turn to slime—clogged emitters, coated roots, suffocated plants.
|
||||
|
||||
Switching back to mineral salts wasn't giving up; it was waking up. Yields doubled and cleaning went from hours to minutes.
|
||||
22
content/blog/post-10.md
Normal file
22
content/blog/post-10.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: "Alright folks, let’s talk hydro! After two decades wrestling with cannabis in sterile environment..."
|
||||
date: 2025-12-26T10:50:30Z
|
||||
image: "/images/IMG_1534.jpeg"
|
||||
summary: "First off – “sterile” matters when we're talking hydroponics with cannabis because root health IS everything and disease spreads fast in water based s..."
|
||||
---
|
||||
|
||||
First off – “sterile” matters when we're talking hydroponics with cannabis because root health IS everything and disease spreads fast in water based systems without proper controls.. We want to minimize the microbial load so plants focus on *growing*, not fighting stuff off constantly . Think consistent sterilization of reservoirs, tubing & grow media.
|
||||
|
||||
Let’s break down some common sterile-friendly options:
|
||||
* **Deep Water Culture (DWC):** The simplest! Roots dangle in oxygenated nutrient solution; good for beginners and scaling up is relatively easy with multiple buckets/reservoirs.. Sterilization? Easy – fully replace the reservoir weekly, or biweekly depending on plant size. Keep that air pump running strong though - dissolved O2 critical here
|
||||
* **Recirculating Deep Water Culture (RDWC):** DWC but linked together! One central oxygenated nutrient res feeds multiple smaller containers with individual net pots.. More complex setup and requires a little more monitoring, especially for pH drift across the system. Sterilization is still largely reservoir based – though you’ll want to flush all connecting lines weekly when changing out solution
|
||||
* **Drain-to-Waste (DTW):** My personal workhorse! Nutrient flows *through*, not recirculates.. Less risk of disease buildup because old nutrient gets flushed with each watering cycle. You need a bit more nute and water, but the consistency in uptake is worth it for consistent results . Sterilization focuses on initial reservoir prep & ensuring clean tubing/media
|
||||
* **Rockwool Slab / Coco Coir Systems:** Technically *hydroponic*, even though they use an inert growing medium.. Excellent control over root zone environment. Requires careful pH and EC management, but are very forgiving – especially coco coirs which can handle minor fluctuations much better than rock wool . Sterilization of the slabs/bricks before planting is key!
|
||||
|
||||
**Troubleshooting 101:** Don’t panic when things go sideways (and they will!). Here's what I see most:
|
||||
* **Root Rot**: Usually caused by Pythium or Fusarium. Often from low dissolved oxygen, high temperatures and not enough sterilization.. Check your water temp; aim for under 75F/24C , bump up aeration & replace that solution! Consider a hydrogen peroxide addition to the reservoir (3% H₂O₂, ~1 tsp per gallon)
|
||||
* **pH Drift:** Common in recirculating systems. Calibrate you pH meter regularly, and check it at multiple points within your system.. Check for algae growth too – light hitting nutrient solutions encourages them & throws off readings!
|
||||
***Nutrient Lockout**: Happens when salts build up or a specific element is unavailable to the plant . Flush with plain PH balanced water. If that doesn’t work, reassess you EC/PPM and make sure your ratios are correct for what growth stage they're in
|
||||
|
||||
|
||||
Bottom line? Sterile hydroponics takes some attention but yields amazing results! Don’t be afraid experiment & document everything - it pays dividends later down the road when scaling up or optimizing a system.
|
||||
30
content/blog/post-11.md
Normal file
30
content/blog/post-11.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Alright, let’s talk roots – specifically how getting oxygen *to* those cannabis roots is the sing..."
|
||||
date: 2025-12-26T16:48:36Z
|
||||
image: "/images/IMG_8377.jpeg"
|
||||
summary: "Everyone focuses so much on lights and nutrient formulas – crucial stuff sure - but if your root zone is suffocating in anaerobic conditions you're le..."
|
||||
---
|
||||
|
||||
Everyone focuses so much on lights and nutrient formulas – crucial stuff sure - but if your root zone is suffocating in anaerobic conditions you're leaving serious potential untapped.. Cannabis *loves* oxygen, especially when it’s pulling nutrients directly from the water column instead of sifting through soil.
|
||||
|
||||
|
||||
|
||||
**So how do we maximize that vital O2? Here are a few techniques I swear by:**
|
||||
|
||||
1.**Consistent Water Temperature is Key**: Aim for between 68-74°F (around ~20 Celsius). Warmer temps hold less dissolved oxygen, colder slows nutrient uptake.. A simple aquarium heater with an inline thermostat will do the trick. Don’t cheap out here; stability matters more than fancy features
|
||||
|
||||
2.**Air Stone Power – But Size Matters**: Everyone uses air stones in DWC! Great start,. The mistake I see a lot is using too *few* or ones that are clogged.. Get enough to create consistent, but not violent turbulence. Think gentle bubbling throughout the entire reservoir - it should look like its lightly simmering
|
||||
|
||||
3.**Hydrogen Peroxide (H2O2) – A Sterile Boost**: I use H₂0₂, at a low dose of 1-2 tsp per gallon every few days,. It breaks down into water and oxygen, giving those roots an extra boost. Plus helps keeps the system cleaner reducing root rot.. Just don’t overdo it - too much can shock them!
|
||||
|
||||
4.**Water Change Frequency**: A lot depends on your plant size & nutrient uptake but I always aim for a complete reservoir change every 7-10 days, especially during peak bloom,. This replenishes oxygen and prevents salt buildup. Don't just top off – *replace* the water completely.. It’s more work upfront that yields big results
|
||||
|
||||
5.**Root Mass Inspection:** When you do your changes pull out some roots! Healthy cannabis root are bright white & fluffy - a sign of healthy O2 uptake . Brown, slimy mush is an indicator something'a amiss
|
||||
|
||||
|
||||
|
||||
These aren’t groundbreaking secrets,. but they *are* often overlooked. Sterile hydroponics isn't just about the nutrients; it requires active management to create and maintain that ideal oxygen-rich environment for your cannabis roots.. Nail this part down ,and you will see a significant difference in vigor, growth rate & ultimately – bud density!
|
||||
|
||||
|
||||
|
||||
I’ll dive deeper into sterile system sanitation next time. Until then happy growing folks!.
|
||||
12
content/blog/post-2.md
Normal file
12
content/blog/post-2.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "The Truth About H2O2 (Hydrogen Peroxide)"
|
||||
date: 2024-02-15T10:00:00Z
|
||||
image: "/images/IMG_9769.jpeg"
|
||||
summary: "It's a powerful tool, but it's not a long-term solution. Here is why."
|
||||
---
|
||||
|
||||
Hydrogen Peroxide is the flamethrower of the hydro world. It kills everything—good and bad—on contact.
|
||||
|
||||
Its half-life in a nutrient solution is tiny. Within hours it's just water and oxygen. If you've got a dirty pump or line feeding pathogens, H2O2 will clear it for a moment and the colony will be back by morning.
|
||||
|
||||
I use H2O2 for *cleaning equipment between runs*. For daily maintenance, I prefer Hypochlorous Acid.
|
||||
12
content/blog/post-3.md
Normal file
12
content/blog/post-3.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "My Battle with Pythium (and How I Won)"
|
||||
date: 2024-03-01T10:00:00Z
|
||||
image: "/images/IMG_6475.jpeg"
|
||||
summary: "Root rot isn't a death sentence if you catch it early and treat it aggressively."
|
||||
---
|
||||
|
||||
It started as slight discoloration on the root tips. I ignored it. Two days later the whole reservoir smelled like a swamp. Pythium moves fast.
|
||||
|
||||
My mistake was trusting "beneficial bacteria" in a system running too warm (75°F). Pythium loves warm, low-oxygen soup.
|
||||
|
||||
The fix? A water chiller. Dropping to 66°F was the turning point. Cold water holds more oxygen, and Pythium hates that.
|
||||
13
content/blog/post-4.md
Normal file
13
content/blog/post-4.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: "VPD Explained for Complete Beginners"
|
||||
date: 2024-03-20T10:00:00Z
|
||||
image: "/images/IMG_9449.jpeg"
|
||||
summary: "Temperature and humidity are connected. Learn how to hit the sweet spot."
|
||||
---
|
||||
|
||||
VPD stands for Vapor Pressure Deficit. Think of it as the air's drying power.
|
||||
|
||||
If the air is too dry (high VPD), plants sweat too much and slam their stomata to save water. Growth stops.
|
||||
If the air is too humid (low VPD), they barely sweat. No water movement means no calcium uptake.
|
||||
|
||||
You want the Goldilocks zone. For early veg, aim around 0.8 kPa. For flower, push 1.2–1.4 kPa.
|
||||
10
content/blog/post-5.md
Normal file
10
content/blog/post-5.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Review: The Best pH Pen Under $100"
|
||||
date: 2024-04-05T10:00:00Z
|
||||
image: "/images/IMG_9413.jpeg"
|
||||
summary: "Stop buying the yellow $10 pens. They are costing you your harvest."
|
||||
---
|
||||
|
||||
I've used BlueLab, Hanna, and the cheap yellow generics. The value winner? The Apera PH60.
|
||||
|
||||
It has a replaceable probe, decent calibration stability, and it's waterproof. The most important feature of a pH pen is trust. If you can't trust the reading (and keep it calibrated), you're flying blind.
|
||||
10
content/blog/post-6.md
Normal file
10
content/blog/post-6.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Why Your Reservoir Smells Bad"
|
||||
date: 2024-04-20T10:00:00Z
|
||||
image: "/images/IMG_2870.jpeg"
|
||||
summary: "Healthy roots smell like fresh rain or cut potatoes. Swamp smell means trouble."
|
||||
---
|
||||
|
||||
If you open your res and it smells like sulfur or rotten eggs, you have anaerobic bacteria. These thrive in low-oxygen water.
|
||||
|
||||
The immediate fix? Add more air. Get a bigger air pump and more stones. You can't really have "too much" dissolved oxygen in a home setup. Then figure out why the system got that stale in the first place.
|
||||
11
content/blog/post-7.md
Normal file
11
content/blog/post-7.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Rockwool vs. Coco Coir: A Sterile Perspective"
|
||||
date: 2024-05-10T10:00:00Z
|
||||
image: "/images/IMG_9612 2.jpeg"
|
||||
summary: "Why I prefer inert media for precise control."
|
||||
---
|
||||
|
||||
Coco is great, but it's organic. It comes with its own cation exchange capacity (CEC), meaning it holds onto Calcium and Magnesium. That's an extra variable in your math.
|
||||
|
||||
Rockwool is basalt rock spun into fiber. It is chemically inert. It holds nothing. If I pour 100ppm of Nitrogen in, 100ppm is available to the plant. For scientific growing, Rockwool wins.
|
||||
If you want sterile, rockwool plays nice; coco fights back.
|
||||
11
content/blog/post-8.md
Normal file
11
content/blog/post-8.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Automating pH: Is it Worth It?"
|
||||
date: 2024-06-01T10:00:00Z
|
||||
image: "/images/IMG_9768.jpeg"
|
||||
summary: "Dosers are cool, but they can fail. Should you trust a robot with your crop?"
|
||||
---
|
||||
|
||||
I love automation, but I always tell new growers: **Don't automate what you don't understand.**
|
||||
|
||||
If you don't know *why* your pH is drifting up (plants eating nitrates) or down (root rot or ammonia), a doser will just mask the problem until it's too late. Master manual control first. Then get the robot.
|
||||
Once you can predict the drift before it happens, then let the robot babysit.
|
||||
22
content/blog/post-9.md
Normal file
22
content/blog/post-9.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: "Dialing EC and pH in Sterile Hydro"
|
||||
date: 2025-12-26T09:22:47Z
|
||||
image: "/images/IMG_1439.jpeg"
|
||||
summary: "How I set EC and pH, what the drifts mean, and how to log it without losing your mind."
|
||||
---
|
||||
|
||||
Sterile growing means zero buffer. EC and pH are your steering wheel.
|
||||
|
||||
**EC (electrical conductivity):** total salts/ppm. I keep veg around 1.6–2.0 EC and push 2.4–3.0 in flower depending on strain and light intensity. Don't chase numbers blind—watch the leaves. Light green with stalled growth? You probably underfed. Dark, clawed tips? Back off.
|
||||
|
||||
**pH:** Cannabis likes slightly acidic solution. My sweet spot is 5.8–6.2 in the reservoir. Calibrate your meter often; cheap meters can burn you faster than a bad dab.
|
||||
|
||||
**Common screw-up:** adjusting pH without checking EC first. If the soup is too hot or too weak, pH tweaks are lipstick on a pig.
|
||||
|
||||
**Practical tips:**
|
||||
- Start low and creep up. Easier to add salts than undo burn.
|
||||
- Check pH daily. Check EC every couple days or any time the plants look different.
|
||||
- Track everything—reservoir swaps, nutrient adds, readings. Your logbook will solve next week's mystery smell.
|
||||
|
||||
Happy growing,
|
||||
Dr Jones
|
||||
11
content/chat.md
Normal file
11
content/chat.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Chat with Dr. Jones AI"
|
||||
date: 2024-12-26T12:00:00Z
|
||||
draft: false
|
||||
layout: "chat"
|
||||
---
|
||||
|
||||
# Chat with Dr. Jones AI
|
||||
|
||||
Get instant answers about sterile hydroponics, cannabis cultivation, and growing techniques from an AI trained on 20 years of experience.
|
||||
|
||||
61
content/contact.md
Normal file
61
content/contact.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "Contact"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
---
|
||||
|
||||
No assistant. No gatekeeper. If you email me, you're talking to the same dude cleaning his reservoirs.
|
||||
|
||||
### How to Reach Me
|
||||
|
||||
<div class="membership-only" style="display: none;">
|
||||
**Email:** [indianaholmes@thetempleofdoom.com](mailto:indianaholmes@thetempleofdoom.com)
|
||||
</div>
|
||||
|
||||
<div class="guest-only" style="display: block;">
|
||||
**Email:** <span class="text-gray-400">Available to members only</span>
|
||||
</div>
|
||||
|
||||
If you're looking for a consultation, hit the form below or email me directly (members only). I usually reply within 24 hours on weekdays unless I'm mid-harvest.
|
||||
|
||||
**Quick questions:** Check the [Blog](/blog) first. I might have already ranted about it there.
|
||||
|
||||
---
|
||||
|
||||
<!-- Paywall Message -->
|
||||
<div class="paywall-message mt-8 bg-gray-800 rounded-lg p-8 border border-gray-700 text-center" style="display: none;">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">Members Only</h2>
|
||||
<p class="text-gray-300 mb-6">
|
||||
The contact form is available exclusively to members. Sign up to get direct access to email me.
|
||||
</p>
|
||||
<a href="/membership/" class="inline-flex items-center px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors">
|
||||
Become a Member
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form (Members Only) -->
|
||||
<form id="contact-form" class="paywall-content mt-8 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-8" style="display: none;">
|
||||
<div class="sm:col-span-2">
|
||||
<label for="name" class="block text-sm font-medium text-gray-300">Name</label>
|
||||
<div class="mt-1">
|
||||
<input type="text" name="name" id="name" autocomplete="name" required class="py-3 px-4 block w-full bg-gray-800 border border-gray-700 rounded-md text-gray-100 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label for="email" class="block text-sm font-medium text-gray-300">Your Email</label>
|
||||
<div class="mt-1">
|
||||
<input type="email" name="email" id="email" autocomplete="email" required class="py-3 px-4 block w-full bg-gray-800 border border-gray-700 rounded-md text-gray-100 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label for="message" class="block text-sm font-medium text-gray-300">Message</label>
|
||||
<div class="mt-1">
|
||||
<textarea id="message" name="message" rows="4" required class="py-3 px-4 block w-full bg-gray-800 border border-gray-700 rounded-md text-gray-100 focus:ring-primary focus:border-primary"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<button type="submit" class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-primary hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
10
content/earnings.md
Normal file
10
content/earnings.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Potential Earnings Calculator"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
layout: "earnings"
|
||||
---
|
||||
|
||||
# Discover Your Potential Earnings
|
||||
|
||||
Play with this calculator to see what a sterile run can net you. My baseline: **1.5lb of cannabis from a 2x2.5x7 space**. It's a sanity check, not a lottery ticket.
|
||||
4
content/library/_index.md
Normal file
4
content/library/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Library"
|
||||
layout: "library"
|
||||
---
|
||||
7
content/membership.md
Normal file
7
content/membership.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Membership"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
layout: "membership"
|
||||
---
|
||||
|
||||
36
content/offerings.md
Normal file
36
content/offerings.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "What I Bring to the Table"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
---
|
||||
|
||||
# What I Bring to the Table
|
||||
|
||||
I'm not here to feed you Reddit folklore. I'm a mellow science nerd who's been running sterile hydro in a legal state for two decades and smoking the proof. I grow the cleanest, loudest plants I can and teach the same way.
|
||||
|
||||
## The Real Deal
|
||||
|
||||
I don't hand out generic templates. I give you the guts and know-how to trust your own room. DIY tutorials tuned to your exact space and gear so you can grow anything without guessing.
|
||||
|
||||
No more random advice that costs you harvests. You get a direct line to someone who's seen the weird problems, fixed them, and knows what your plants actually need.
|
||||
|
||||
## Sterile Hydroponics: The Methodology
|
||||
|
||||
The foundation is **sterile hydroponics**. It's a mindset for your grow room: clean inputs, clean plumbing, and no biological wildcards.
|
||||
|
||||
The secrets are in the details I've refined over twenty years—timing, dosing, cleaning cadence—the little moves that separate award-winning harvests from "good enough."
|
||||
|
||||
Those details are what you get when we work together: the protocols, the timing, the tricks that turn a decent grow into something special.
|
||||
|
||||
## What You Actually Get
|
||||
|
||||
- **Personalized guidance** built for your setup
|
||||
- **DIY walkthroughs** that match your space and gear
|
||||
- **Real fixes** to real problems, not theory for theory's sake
|
||||
- **Hard-earned playbooks** from 20 years hands-on
|
||||
- **Confidence** to run clean without second-guessing
|
||||
- **A direct line** to someone who lives this every day
|
||||
|
||||
I'm not here to babysit every step. I'm here to hand you the tools and courage to run a sterile, high-output grow on your own.
|
||||
|
||||
The secrets are real. The results are real. Ready to stop guessing and start growing on purpose?
|
||||
19
content/services/1-consultation.md
Normal file
19
content/services/1-consultation.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "One-on-One Tutoring"
|
||||
date: 2024-12-24T01:00:00Z
|
||||
price: "$20"
|
||||
price_period: "/ hour"
|
||||
cta: "Book Session"
|
||||
foxy_link: "https://example.foxycart.com/cart?name=Tutoring&price=20"
|
||||
---
|
||||
|
||||
A 60-minute video call to untangle your exact situation and leave you with a plan.
|
||||
|
||||
**What's Included:**
|
||||
- Quick diagnosis of deficiencies, pests, or pathogens.
|
||||
- Sterile nutrient recipe tune-up for your reservoir.
|
||||
- Protocol check on how you're cleaning and running the system.
|
||||
- Session recording so you don't miss the details.
|
||||
|
||||
**Who it's for:**
|
||||
Home growers stuck on a specific problem who want straight answers instead of forum guesses.
|
||||
19
content/services/2-design.md
Normal file
19
content/services/2-design.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "System Design Review"
|
||||
date: 2024-12-24T02:00:00Z
|
||||
price: "$200"
|
||||
price_period: "/ project"
|
||||
cta: "Submit Design"
|
||||
foxy_link: "https://example.foxycart.com/cart?name=DesignReview&price=200"
|
||||
---
|
||||
|
||||
Before you build, let me sanity-check your engineering so it stays sterile and easy to maintain.
|
||||
|
||||
**What's Included:**
|
||||
- Full schematic audit (plumbing, lighting, HVAC).
|
||||
- Parts and compatibility check.
|
||||
- Sterility risk map.
|
||||
- Written report with 3-5 actionable fixes.
|
||||
|
||||
**Who it's for:**
|
||||
Growers planning a new room or major retrofit who want to avoid expensive rework and keep everything lab-clean from day one.
|
||||
19
content/services/3-mentorship.md
Normal file
19
content/services/3-mentorship.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "Ongoing Mentorship"
|
||||
date: 2024-12-24T03:00:00Z
|
||||
price: "$500"
|
||||
price_period: "/ month"
|
||||
cta: "Request Mentorship"
|
||||
foxy_link: "https://example.foxycart.com/cart?name=Mentorship&price=500&sub_frequency=1m"
|
||||
---
|
||||
|
||||
I become your fractional Master Grower for an entire crop cycle.
|
||||
|
||||
**What's Included:**
|
||||
- Weekly 30-min check-in calls.
|
||||
- Unlimited email/text support (within business hours).
|
||||
- Real-time crop steering on EC, pH, and canopy calls.
|
||||
- Harvest and curing guidance.
|
||||
|
||||
**Who it's for:**
|
||||
Serious growers chasing competition-quality results who want a safety net and someone to geek out with along the way.
|
||||
4
content/services/_index.md
Normal file
4
content/services/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Services"
|
||||
description: "Professional Hydroponic Consulting"
|
||||
---
|
||||
6
content/testimonials.md
Normal file
6
content/testimonials.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "Testimonials"
|
||||
date: 2024-12-24T12:00:00Z
|
||||
draft: false
|
||||
layout: "testimonials"
|
||||
---
|
||||
514
data/library.yaml
Normal file
514
data/library.yaml
Normal file
@@ -0,0 +1,514 @@
|
||||
- title: 'Hydroponics Garden: The Complete DIY Guide For Beginners To Learn How To
|
||||
Build A System To Grow Plants'
|
||||
filename: Hydroponics Garden The Complete DIY Guide For Beginners To Learn How To
|
||||
Build A System To Grow Plants.pdf
|
||||
description: 'Educational resource: Hydroponics Garden: The Complete DIY Guide For
|
||||
Beginners To Learn How To Build A System To Grow Plants'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponics Garden The Complete DIY Guide For Beginners To Learn
|
||||
How To Build A System To Grow Plants.pdf
|
||||
- title: Beginner's Guide to Growing Cannabis and Making Your Own Healing Remedies
|
||||
filename: Beginner's Guide to Growing Cannabis and Making Your Own Healing Remedies.epub
|
||||
description: 'Educational resource: Beginner''s Guide to Growing Cannabis and Making
|
||||
Your Own Healing Remedies'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Beginner's Guide to Growing Cannabis and Making Your Own Healing
|
||||
Remedies.epub
|
||||
- title: Totally Organic Hydroponics
|
||||
filename: Totally Organic Hydroponics.pdf
|
||||
description: 'Educational resource: Totally Organic Hydroponics'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Totally Organic Hydroponics.pdf
|
||||
- title: 'Clearing the Smoke: The Science of Cannabis'
|
||||
filename: Clearing the Smoke The Science of Cannabis.mp4
|
||||
description: 'Educational resource: Clearing the Smoke: The Science of Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Clearing the Smoke The Science of Cannabis.mp4
|
||||
- title: Gardening For Beginners 2024
|
||||
filename: Gardening For Beginners 2024.pdf
|
||||
description: 'Educational resource: Gardening For Beginners 2024'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Gardening For Beginners 2024.pdf
|
||||
- title: Encyclopedia of Garden Plants for Every Location
|
||||
filename: Encyclopedia of Garden Plants for Every Location.pdf
|
||||
description: 'Educational resource: Encyclopedia of Garden Plants for Every Location'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Encyclopedia of Garden Plants for Every Location.pdf
|
||||
- title: 'Cannabis: A Complete Guide'
|
||||
filename: Cannabis A Complete Guide.pdf
|
||||
description: 'Educational resource: Cannabis: A Complete Guide'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis A Complete Guide.pdf
|
||||
- title: Guide to Hydroponics
|
||||
filename: Guide to Hydroponics.epub
|
||||
description: 'Educational resource: Guide to Hydroponics'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Guide to Hydroponics.epub
|
||||
- title: 'Marijuana Harvest: How to Maximize Quality and Yield in Your Cannabis Garden'
|
||||
filename: Marijuana Harvest How to Maximize Quality and Yield in Your Cannabis Garden.epub
|
||||
description: 'Educational resource: Marijuana Harvest: How to Maximize Quality and
|
||||
Yield in Your Cannabis Garden'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Harvest How to Maximize Quality and Yield in Your Cannabis
|
||||
Garden.epub
|
||||
- title: The Ecology of Plants
|
||||
filename: The Ecology of Plants.pdf
|
||||
description: 'Educational resource: The Ecology of Plants'
|
||||
category: Science
|
||||
date: '2024-03-25'
|
||||
file: /library/The Ecology of Plants.pdf
|
||||
- title: 'Hydroponic Food Production: A Definitive Guidebook for the Commercial Hydroponic
|
||||
Grower'
|
||||
filename: Hydroponic Food Production A Definitive Guidebook for the Commercial Hydroponic
|
||||
Grower.pdf
|
||||
description: 'Educational resource: Hydroponic Food Production: A Definitive Guidebook
|
||||
for the Commercial Hydroponic Grower'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponic Food Production A Definitive Guidebook for the Commercial
|
||||
Hydroponic Grower.pdf
|
||||
- title: 'Grow Great Weed: Personal & Medical Marijuana Indoor Outdoor Grower Big
|
||||
Bud Bible'
|
||||
filename: Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower Big
|
||||
Bud Bible.pdf
|
||||
description: 'Educational resource: Grow Great Weed: Personal & Medical Marijuana
|
||||
Indoor Outdoor Grower Big Bud Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower
|
||||
Big Bud Bible.pdf
|
||||
- title: Scanned Image
|
||||
filename: Scanned Image.pdf
|
||||
description: 'Educational resource: Scanned Image'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Scanned Image.pdf
|
||||
- title: 100 Best Cannabis Strains
|
||||
filename: 100 Best Cannabis Strains.pdf
|
||||
description: 'Educational resource: 100 Best Cannabis Strains'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/100 Best Cannabis Strains.pdf
|
||||
- title: Downmagaz.net
|
||||
filename: Downmagaz.net.pdf
|
||||
description: 'Educational resource: Downmagaz.net'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Downmagaz.net.pdf
|
||||
- title: The Cannabis Grow Bible
|
||||
filename: The Cannabis Grow Bible.pdf
|
||||
description: 'Educational resource: The Cannabis Grow Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/The Cannabis Grow Bible.pdf
|
||||
- title: Diagnosing Hemp and Cannabis Crop Diseases
|
||||
filename: Diagnosing Hemp and Cannabis Crop Diseases.pdf
|
||||
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Diagnosing Hemp and Cannabis Crop Diseases.pdf
|
||||
- title: Guide to Hydroponics
|
||||
filename: Guide to Hydroponics.pdf
|
||||
description: 'Educational resource: Guide to Hydroponics'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Guide to Hydroponics.pdf
|
||||
- title: 'Hydroponics Garden: The Complete DIY Guide For Beginners To Learn How To
|
||||
Build A System To Grow Plants'
|
||||
filename: Hydroponics Garden The Complete DIY Guide For Beginners To Learn How To
|
||||
Build A System To Grow Plants.pdf
|
||||
description: 'Educational resource: Hydroponics Garden: The Complete DIY Guide For
|
||||
Beginners To Learn How To Build A System To Grow Plants'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponics Garden The Complete DIY Guide For Beginners To Learn
|
||||
How To Build A System To Grow Plants.pdf
|
||||
- title: 'How to Grow Marijuana: 3 Books in 1 - The Complete Guide to Growing Top-Quality
|
||||
Weed Indoors and Outdoors'
|
||||
filename: How to Grow Marijuana 3 Books in 1 - The Complete Guide to Growing Top-Quality
|
||||
Weed Indoors and Outdoors.pdf
|
||||
description: 'Educational resource: How to Grow Marijuana: 3 Books in 1 - The Complete
|
||||
Guide to Growing Top-Quality Weed Indoors and Outdoors'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/How to Grow Marijuana 3 Books in 1 - The Complete Guide to Growing
|
||||
Top-Quality Weed Indoors and Outdoors.pdf
|
||||
- title: Hydroponics Gardening Guide
|
||||
filename: Hydroponics Gardening Guide.pdf
|
||||
description: 'Educational resource: Hydroponics Gardening Guide'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponics Gardening Guide.pdf
|
||||
- title: Jesse Ventura's Marijuana Manifesto
|
||||
filename: Jesse Ventura's Marijuana Manifesto.pdf
|
||||
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Jesse Ventura's Marijuana Manifesto.pdf
|
||||
- title: True Living Organics
|
||||
filename: True Living Organics.pdf
|
||||
description: 'Educational resource: True Living Organics'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/True Living Organics.pdf
|
||||
- title: 'DIY Home Hydroponics For Beginners: The Essential Guide To Turn Your Backyard
|
||||
Into A Farm'
|
||||
filename: DIY Home Hydroponics For Beginners The Essential Guide To Turn Your Backyard
|
||||
Into A Farm.pdf
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners: The Essential
|
||||
Guide To Turn Your Backyard Into A Farm'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Home Hydroponics For Beginners The Essential Guide To Turn Your
|
||||
Backyard Into A Farm.pdf
|
||||
- title: 'DIY Home Hydroponics For Beginners: The Essential Guide To Turn Your Backyard
|
||||
Into A Farm'
|
||||
filename: DIY Home Hydroponics For Beginners The Essential Guide To Turn Your Backyard
|
||||
Into A Farm.pdf
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners: The Essential
|
||||
Guide To Turn Your Backyard Into A Farm'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Home Hydroponics For Beginners The Essential Guide To Turn Your
|
||||
Backyard Into A Farm.pdf
|
||||
- title: What a Plant Knows
|
||||
filename: What a Plant Knows.pdf
|
||||
description: 'Educational resource: What a Plant Knows'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/What a Plant Knows.pdf
|
||||
- title: 'The Biology of Marijuana: From Gene to Behavior'
|
||||
filename: The Biology of Marijuana From Gene to Behavior.pdf
|
||||
description: 'Educational resource: The Biology of Marijuana: From Gene to Behavior'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/The Biology of Marijuana From Gene to Behavior.pdf
|
||||
- title: 'The Cannabis Breeder''s Bible: The Definitive Guide to Marijuana Genetics,
|
||||
Cannabis Botany and Creating Strains for the Seed Market'
|
||||
filename: The Cannabis Breeder's Bible The Definitive Guide to Marijuana Genetics,
|
||||
Cannabis Botany and Creating Strains for the Seed Market.epub
|
||||
description: 'Educational resource: The Cannabis Breeder''s Bible: The Definitive
|
||||
Guide to Marijuana Genetics, Cannabis Botany and Creating Strains for the Seed
|
||||
Market'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/The Cannabis Breeder's Bible The Definitive Guide to Marijuana Genetics,
|
||||
Cannabis Botany and Creating Strains for the Seed Market.epub
|
||||
- title: 'Cannabis Alchemy: The Art of Modern Hashmaking'
|
||||
filename: Cannabis Alchemy The Art of Modern Hashmaking.pdf
|
||||
description: 'Educational resource: Cannabis Alchemy: The Art of Modern Hashmaking'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis Alchemy The Art of Modern Hashmaking.pdf
|
||||
- title: 'Marijuana Botany: An Advanced Study - The Propagation and Breeding of Distinctive
|
||||
Cannabis'
|
||||
filename: Marijuana Botany An Advanced Study - The Propagation and Breeding of Distinctive
|
||||
Cannabis.pdf
|
||||
description: 'Educational resource: Marijuana Botany: An Advanced Study - The Propagation
|
||||
and Breeding of Distinctive Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Botany An Advanced Study - The Propagation and Breeding
|
||||
of Distinctive Cannabis.pdf
|
||||
- title: DIY Home Hydroponics For Beginners
|
||||
filename: DIY Home Hydroponics For Beginners.epub
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Home Hydroponics For Beginners.epub
|
||||
- title: Diagnosing Hemp and Cannabis Crop Diseases
|
||||
filename: Diagnosing Hemp and Cannabis Crop Diseases.pdf
|
||||
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Diagnosing Hemp and Cannabis Crop Diseases.pdf
|
||||
- title: Handbook of Cannabis
|
||||
filename: Handbook of Cannabis.pdf
|
||||
description: 'Educational resource: Handbook of Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Handbook of Cannabis.pdf
|
||||
- title: 'Hydroponics: A Standard Methodology for Plant Biological Researches'
|
||||
filename: Hydroponics A Standard Methodology for Plant Biological Researches.pdf
|
||||
description: 'Educational resource: Hydroponics: A Standard Methodology for Plant
|
||||
Biological Researches'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponics A Standard Methodology for Plant Biological Researches.pdf
|
||||
- title: 'Cannabis Sativa Volume 3: The Essential Guide to the World''s Finest Marijuana
|
||||
Strains'
|
||||
filename: Cannabis Sativa Volume 3 The Essential Guide to the World's Finest Marijuana
|
||||
Strains.epub
|
||||
description: 'Educational resource: Cannabis Sativa Volume 3: The Essential Guide
|
||||
to the World''s Finest Marijuana Strains'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis Sativa Volume 3 The Essential Guide to the World's Finest
|
||||
Marijuana Strains.epub
|
||||
- title: 'Growing Marijuana Mastery: The Easiest and Complete Handbook to Growing
|
||||
Weed Indoor and Outdoor'
|
||||
filename: Growing Marijuana Mastery The Easiest and Complete Handbook to Growing
|
||||
Weed Indoor and Outdoor.pdf
|
||||
description: 'Educational resource: Growing Marijuana Mastery: The Easiest and Complete
|
||||
Handbook to Growing Weed Indoor and Outdoor'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Growing Marijuana Mastery The Easiest and Complete Handbook to Growing
|
||||
Weed Indoor and Outdoor.pdf
|
||||
- title: Gardening Indoors with Soil & Hydroponics, Fifth Edition
|
||||
filename: Gardening Indoors with Soil & Hydroponics, Fifth Edition.pdf
|
||||
description: 'Educational resource: Gardening Indoors with Soil & Hydroponics, Fifth
|
||||
Edition'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Gardening Indoors with Soil & Hydroponics, Fifth Edition.pdf
|
||||
- title: 'Cannabis Chemistry and Biology: Fundamentals'
|
||||
filename: Cannabis Chemistry and Biology Fundamentals.pdf
|
||||
description: 'Educational resource: Cannabis Chemistry and Biology: Fundamentals'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis Chemistry and Biology Fundamentals.pdf
|
||||
- title: Nutrients for Hydroponics Tissue Culture
|
||||
filename: Nutrients for Hydroponics Tissue Culture.pdf
|
||||
description: 'Educational resource: Nutrients for Hydroponics Tissue Culture'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Nutrients for Hydroponics Tissue Culture.pdf
|
||||
- title: 'Marijuana Grow Basics: The Easy Guide for Cannabis Aficionados'
|
||||
filename: Marijuana Grow Basics The Easy Guide for Cannabis Aficionados.pdf
|
||||
description: 'Educational resource: Marijuana Grow Basics: The Easy Guide for Cannabis
|
||||
Aficionados'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Grow Basics The Easy Guide for Cannabis Aficionados.pdf
|
||||
- title: 'DIY Home Hydroponics For Beginners: The Essential Guide To Turn Your Backyard
|
||||
Into A Farm'
|
||||
filename: DIY Home Hydroponics For Beginners The Essential Guide To Turn Your Backyard
|
||||
Into A Farm.epub
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners: The Essential
|
||||
Guide To Turn Your Backyard Into A Farm'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Home Hydroponics For Beginners The Essential Guide To Turn Your
|
||||
Backyard Into A Farm.epub
|
||||
- title: '100 Best Cannabis Strains: A Pocket Guide for Medicinal and Recreational
|
||||
Use'
|
||||
filename: 100 Best Cannabis Strains A Pocket Guide for Medicinal and Recreational
|
||||
Use.epub
|
||||
description: 'Educational resource: 100 Best Cannabis Strains: A Pocket Guide for
|
||||
Medicinal and Recreational Use'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/100 Best Cannabis Strains A Pocket Guide for Medicinal and Recreational
|
||||
Use.epub
|
||||
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
|
||||
filename: '#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
|
||||
Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
|
||||
filename: '#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
|
||||
Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
|
||||
filename: '#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
|
||||
Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
|
||||
filename: '#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
|
||||
Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
|
||||
filename: '#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
|
||||
Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
|
||||
filename: '#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
|
||||
Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Jesse Ventura's Marijuana Manifesto
|
||||
filename: Jesse Ventura's Marijuana Manifesto.epub
|
||||
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Jesse Ventura's Marijuana Manifesto.epub
|
||||
- title: 'Grow Great Weed: Personal & Medical Marijuana Indoor Outdoor Grower Big
|
||||
Bud Bible'
|
||||
filename: Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower Big
|
||||
Bud Bible.pdf
|
||||
description: 'Educational resource: Grow Great Weed: Personal & Medical Marijuana
|
||||
Indoor Outdoor Grower Big Bud Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower
|
||||
Big Bud Bible.pdf
|
||||
- title: Hydroponics Gardening Guide
|
||||
filename: Hydroponics Gardening Guide.epub
|
||||
description: 'Educational resource: Hydroponics Gardening Guide'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponics Gardening Guide.epub
|
||||
- title: 'How to Grow Marijuana: 3 Books in 1'
|
||||
filename: How to Grow Marijuana 3 Books in 1.pdf
|
||||
description: 'Educational resource: How to Grow Marijuana: 3 Books in 1'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/How to Grow Marijuana 3 Books in 1.pdf
|
||||
- title: 'Cannabis Grower''s Handbook: The Complete Guide to Marijuana and Hemp Cultivation'
|
||||
filename: Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp Cultivation.pdf
|
||||
description: 'Educational resource: Cannabis Grower''s Handbook: The Complete Guide
|
||||
to Marijuana and Hemp Cultivation'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp
|
||||
Cultivation.pdf
|
||||
- title: 'The Big Book of Buds: More Marijuana Varieties from the World''s Greatest
|
||||
Seed Breeders'
|
||||
filename: The Big Book of Buds More Marijuana Varieties from the World's Greatest
|
||||
Seed Breeders.epub
|
||||
description: 'Educational resource: The Big Book of Buds: More Marijuana Varieties
|
||||
from the World''s Greatest Seed Breeders'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/The Big Book of Buds More Marijuana Varieties from the World's Greatest
|
||||
Seed Breeders.epub
|
||||
- title: 'Marijuana Hydroponics: High-Tech Water Culture'
|
||||
filename: Marijuana Hydroponics High-Tech Water Culture.epub
|
||||
description: 'Educational resource: Marijuana Hydroponics: High-Tech Water Culture'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Hydroponics High-Tech Water Culture.epub
|
||||
- title: 'Closet Cultivator: Indoor Marijuana Cultivation Made Easy'
|
||||
filename: Closet Cultivator Indoor Marijuana Cultivation Made Easy.pdf
|
||||
description: 'Educational resource: Closet Cultivator: Indoor Marijuana Cultivation
|
||||
Made Easy'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Closet Cultivator Indoor Marijuana Cultivation Made Easy.pdf
|
||||
- title: 'What a Plant Knows: A Field Guide to the Senses'
|
||||
filename: What a Plant Knows A Field Guide to the Senses.epub
|
||||
description: 'Educational resource: What a Plant Knows: A Field Guide to the Senses'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/What a Plant Knows A Field Guide to the Senses.epub
|
||||
- title: Handbook of Cannabis Production in Controlled Environments
|
||||
filename: Handbook of Cannabis Production in Controlled Environments.pdf
|
||||
description: 'Educational resource: Handbook of Cannabis Production in Controlled
|
||||
Environments'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Handbook of Cannabis Production in Controlled Environments.pdf
|
||||
- title: The Ecology of Plants, 3rd Edition
|
||||
filename: The Ecology of Plants, 3rd Edition.pdf
|
||||
description: 'Educational resource: The Ecology of Plants, 3rd Edition'
|
||||
category: Science
|
||||
date: '2024-03-25'
|
||||
file: /library/The Ecology of Plants, 3rd Edition.pdf
|
||||
- title: 'Growing Marijuana Mastery: The Advanced Guide to Grow Cannabis'
|
||||
filename: Growing Marijuana Mastery The Advanced Guide to Grow Cannabis.epub
|
||||
description: 'Educational resource: Growing Marijuana Mastery: The Advanced Guide
|
||||
to Grow Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Growing Marijuana Mastery The Advanced Guide to Grow Cannabis.epub
|
||||
- title: 'Marijuana Botany: An Advanced Study - The Propagation and Breeding of Distinctive
|
||||
Cannabis'
|
||||
filename: Marijuana Botany An Advanced Study - The Propagation and Breeding of Distinctive
|
||||
Cannabis.pdf
|
||||
description: 'Educational resource: Marijuana Botany: An Advanced Study - The Propagation
|
||||
and Breeding of Distinctive Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Botany An Advanced Study - The Propagation and Breeding
|
||||
of Distinctive Cannabis.pdf
|
||||
- title: 'Cannabis Grower''s Handbook: The Complete Guide to Marijuana and Hemp Cultivation'
|
||||
filename: Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp Cultivation.epub
|
||||
description: 'Educational resource: Cannabis Grower''s Handbook: The Complete Guide
|
||||
to Marijuana and Hemp Cultivation'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp
|
||||
Cultivation.epub
|
||||
- title: 'True Living Organics: The Ultimate Guide to Growing All-Natural Marijuana
|
||||
Indoors'
|
||||
filename: True Living Organics The Ultimate Guide to Growing All-Natural Marijuana
|
||||
Indoors.epub
|
||||
description: 'Educational resource: True Living Organics: The Ultimate Guide to
|
||||
Growing All-Natural Marijuana Indoors'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/True Living Organics The Ultimate Guide to Growing All-Natural Marijuana
|
||||
Indoors.epub
|
||||
- title: Marijuana Chemistry
|
||||
filename: Marijuana Chemistry.pdf
|
||||
description: 'Educational resource: Marijuana Chemistry'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Chemistry.pdf
|
||||
- title: 'Plant Science: An Introduction to Botany'
|
||||
filename: Plant Science An Introduction to Botany.pdf
|
||||
description: 'Educational resource: Plant Science: An Introduction to Botany'
|
||||
category: Science
|
||||
date: '2024-03-25'
|
||||
file: /library/Plant Science An Introduction to Botany.pdf
|
||||
- title: 'Hydroponic Food Production: A Definitive Guidebook, 7th Edition'
|
||||
filename: Hydroponic Food Production A Definitive Guidebook, 7th Edition.pdf
|
||||
description: 'Educational resource: Hydroponic Food Production: A Definitive Guidebook,
|
||||
7th Edition'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponic Food Production A Definitive Guidebook, 7th Edition.pdf
|
||||
- title: 'Higher: The Lore, Legends, and Legacy of Cannabis'
|
||||
filename: Higher The Lore, Legends, and Legacy of Cannabis.epub
|
||||
description: 'Educational resource: Higher: The Lore, Legends, and Legacy of Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Higher The Lore, Legends, and Legacy of Cannabis.epub
|
||||
- title: Marijuana Horticulture Complete
|
||||
filename: Marijuana Horticulture Complete.pdf
|
||||
description: 'Educational resource: Marijuana Horticulture Complete'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture Complete.pdf
|
||||
- title: Commercial Hydroponics, 3rd Edition
|
||||
filename: Commercial Hydroponics, 3rd Edition.pdf
|
||||
description: 'Educational resource: Commercial Hydroponics, 3rd Edition'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Commercial Hydroponics, 3rd Edition.pdf
|
||||
- title: 'DIY Aquaponics Marijuana Growing at Home: Learn to Grow Most Potent THC
|
||||
Cannabis Ever!'
|
||||
filename: DIY Aquaponics Marijuana Growing at Home Learn to Grow Most Potent THC
|
||||
Cannabis Ever!.epub
|
||||
description: 'Educational resource: DIY Aquaponics Marijuana Growing at Home: Learn
|
||||
to Grow Most Potent THC Cannabis Ever!'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Aquaponics Marijuana Growing at Home Learn to Grow Most Potent
|
||||
THC Cannabis Ever!.epub
|
||||
BIN
db/members.db
Normal file
BIN
db/members.db
Normal file
Binary file not shown.
74
hugo.yaml
Normal file
74
hugo.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
baseURL: 'https://hydrolord.thetempleofdoom.com/'
|
||||
languageCode: 'en-us'
|
||||
title: 'Pure Hydroponics'
|
||||
theme: []
|
||||
|
||||
params:
|
||||
description: 'Sterile hydroponics expert with 20 years of experience helping growers achieve award-winning results. Learn sterile hydroponic protocols for cannabis and all plants.'
|
||||
author: 'Dr. Jones'
|
||||
|
||||
build:
|
||||
writeStats: true
|
||||
|
||||
module:
|
||||
mounts:
|
||||
- source: content
|
||||
target: content
|
||||
- source: layouts
|
||||
target: layouts
|
||||
- source: static
|
||||
target: static
|
||||
|
||||
ignoreLogs: ['warning-goldmark-raw-html']
|
||||
|
||||
# Menu Configuration
|
||||
menu:
|
||||
main:
|
||||
- identifier: home
|
||||
name: Home
|
||||
url: /
|
||||
weight: 10
|
||||
- identifier: about
|
||||
name: About Me
|
||||
url: /about/
|
||||
weight: 20
|
||||
- identifier: services
|
||||
name: Services
|
||||
url: /services/
|
||||
weight: 30
|
||||
- identifier: membership
|
||||
name: Membership
|
||||
url: /membership/
|
||||
weight: 32
|
||||
- identifier: offerings
|
||||
name: Offerings
|
||||
url: /offerings/
|
||||
weight: 35
|
||||
- identifier: earnings
|
||||
name: Earnings Calculator
|
||||
url: /earnings/
|
||||
weight: 36
|
||||
- identifier: chat
|
||||
name: Chat
|
||||
url: /chat/
|
||||
weight: 37
|
||||
- identifier: library
|
||||
name: Library
|
||||
url: /library/
|
||||
weight: 40
|
||||
- identifier: albums
|
||||
name: Albums
|
||||
url: /albums/
|
||||
weight: 50
|
||||
- identifier: blog
|
||||
name: Journal
|
||||
url: /blog/
|
||||
weight: 60
|
||||
- identifier: testimonials
|
||||
name: Testimonials
|
||||
url: /testimonials/
|
||||
weight: 65
|
||||
- identifier: contact
|
||||
name: Contact
|
||||
url: /contact/
|
||||
weight: 70
|
||||
207
layouts/_default/baseof.html
Normal file
207
layouts/_default/baseof.html
Normal file
@@ -0,0 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ .Site.LanguageCode }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src *; img-src * data:; frame-ancestors 'self';">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=()">
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
|
||||
<meta name="title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}">
|
||||
<meta name="description" content="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ else }}{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results. 20 years of experience." }}{{ end }}">
|
||||
<meta name="keywords" content="{{ if .Params.keywords }}{{ delimit .Params.keywords ", " }}{{ else }}sterile hydroponics, hydroponic growing, cannabis cultivation, hydroponic consulting, sterile growing, hydroponic systems{{ end }}">
|
||||
<meta name="author" content="{{ .Site.Params.author | default "Dr. Jones" }}">
|
||||
<link rel="canonical" href="{{ .Permalink }}">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
|
||||
<meta property="og:url" content="{{ .Permalink }}">
|
||||
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
|
||||
<meta property="og:description" content="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ else }}{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results." }}{{ end }}">
|
||||
{{ if .Params.image }}
|
||||
<meta property="og:image" content="{{ .Params.image | absURL }}">
|
||||
{{ else }}
|
||||
<meta property="og:image" content="{{ "/images/IMG_1767.jpeg" | absURL }}">
|
||||
{{ end }}
|
||||
<meta property="og:site_name" content="{{ .Site.Title }}">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="{{ .Permalink }}">
|
||||
<meta property="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
|
||||
<meta property="twitter:description" content="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ else }}{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results." }}{{ end }}">
|
||||
{{ if .Params.image }}
|
||||
<meta property="twitter:image" content="{{ .Params.image | absURL }}">
|
||||
{{ else }}
|
||||
<meta property="twitter:image" content="{{ "/images/IMG_1767.jpeg" | absURL }}">
|
||||
{{ end }}
|
||||
|
||||
<!-- Structured Data (JSON-LD) -->
|
||||
{{ if .IsHome }}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "{{ .Site.Title }}",
|
||||
"url": "{{ .Site.BaseURL }}",
|
||||
"description": "{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results." }}",
|
||||
"sameAs": [
|
||||
"https://www.instagram.com/drj0nes/"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{{ else if .IsPage }}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "{{ if eq .Section "blog" }}BlogPosting{{ else }}Article{{ end }}",
|
||||
"headline": "{{ .Title }}",
|
||||
"datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
|
||||
"dateModified": "{{ if .Lastmod }}{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}{{ end }}",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "{{ .Site.Params.author | default "Dr. Jones" }}"
|
||||
},
|
||||
"description": "{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ end }}"
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
{{ $styles := resources.Get "css/main.css" | css.PostCSS }}
|
||||
<link rel="stylesheet" href="{{ $styles.RelPermalink }}">
|
||||
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Membership System -->
|
||||
<script src="/js/membership.js"></script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<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@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen font-sans" x-data="{ mobileMenuOpen: false }">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="sticky top-0 z-50 bg-gray-900/90 backdrop-blur-md border-b border-gray-800 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<a href="/" class="flex-shrink-0 flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center text-white font-bold text-xl">P</div>
|
||||
<span class="font-bold text-xl tracking-tight text-gray-100">Pure<span class="text-primary">Hydro</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex md:items-center md:space-x-8">
|
||||
{{ range .Site.Menus.main }}
|
||||
<a href="{{ .URL }}" class="text-gray-300 hover:text-primary px-3 py-2 rounded-md text-sm font-medium transition-colors {{ if or (eq $.RelPermalink .URL) (eq $.RelPermalink (printf "%s/" .URL)) }}text-primary font-semibold{{ end }}">
|
||||
{{ .Name }}
|
||||
</a>
|
||||
{{ end }}
|
||||
<!-- Membership Login/Logout -->
|
||||
<div class="membership-login" style="display: none;">
|
||||
<a href="/membership/" class="text-gray-300 hover:text-primary px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
<div class="membership-logout" style="display: none;">
|
||||
<span class="text-gray-300 px-3 py-2 text-sm">
|
||||
<span class="user-name"></span>
|
||||
</span>
|
||||
<button onclick="window.MembershipAPI.logout()" class="text-gray-300 hover:text-primary px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<a href="/services/" class="bg-primary text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-emerald-700 transition-colors shadow-sm hover:shadow-md">
|
||||
Work With Me
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="flex items-center md:hidden">
|
||||
<button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-300 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div x-show="mobileMenuOpen"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2"
|
||||
class="md:hidden bg-gray-900 border-b border-gray-800 shadow-lg absolute w-full"
|
||||
style="display: none;">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||
{{ range .Site.Menus.main }}
|
||||
<a href="{{ .URL }}" class="text-gray-300 hover:text-primary hover:bg-gray-800 block px-3 py-2 rounded-md text-base font-medium">
|
||||
{{ .Name }}
|
||||
</a>
|
||||
{{ end }}
|
||||
<!-- Mobile Membership Login/Logout -->
|
||||
<div class="membership-login" style="display: none;">
|
||||
<a href="/membership/" class="text-gray-300 hover:text-primary hover:bg-gray-800 block px-3 py-2 rounded-md text-base font-medium">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
<div class="membership-logout" style="display: none;">
|
||||
<span class="text-gray-300 block px-3 py-2 text-base">
|
||||
<span class="user-name"></span>
|
||||
</span>
|
||||
<button onclick="window.MembershipAPI.logout()" class="text-gray-300 hover:text-primary hover:bg-gray-800 block px-3 py-2 rounded-md text-base font-medium w-full text-left">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<a href="/services/" class="text-white bg-primary block px-3 py-2 rounded-md text-base font-medium mt-4 text-center">
|
||||
Work With Me
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-grow">
|
||||
{{ block "main" . }}{{ end }}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 border-t border-gray-800 mt-12">
|
||||
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center flex-col md:flex-row gap-6">
|
||||
<div class="text-center md:text-left">
|
||||
<span class="font-bold text-xl tracking-tight text-gray-100">Pure<span class="text-primary">Hydro</span></span>
|
||||
<p class="mt-2 text-sm text-gray-400">Cultivating clean, sterile, and abundant gardens.</p>
|
||||
</div>
|
||||
<div class="flex items-center flex-wrap justify-center gap-4">
|
||||
<a href="https://www.buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-4 py-2 bg-[#FFDD00] hover:bg-[#FFCC00] text-gray-900 rounded-lg font-semibold transition-colors shadow-sm hover:shadow-md">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.18 6.78 7.29 3.96.12 7.22-3.06 7.22-7v-1h.5c1.38 0 2.5-1.12 2.5-2.5S19.88 3 18.5 3zM16 5v3H6V5h10zm2.5 5H18V5h.5c.28 0 .5.22.5.5s-.22.5-.5.5zM4 19h16v2H4v-2z"/>
|
||||
</svg>
|
||||
Buy Me a Coffee
|
||||
</a>
|
||||
<a href="https://www.instagram.com/drj0nes/" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-gray-300">
|
||||
Instagram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
|
||||
© {{ now.Year }} Pure Hydroponics. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
42
layouts/_default/list.html
Normal file
42
layouts/_default/list.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center">
|
||||
<h2 class="text-base text-primary font-semibold tracking-wide uppercase">{{ .Section }}</h2>
|
||||
<p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-100 sm:text-4xl">{{ .Title }}</p>
|
||||
<p class="mt-4 max-w-2xl text-xl text-gray-300 lg:mx-auto">{{ .Description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12">
|
||||
{{ range .Paginator.Pages }}
|
||||
<div class="flex flex-col rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-48 w-full object-cover" src="{{ with .Params.image }}{{ . }}{{ else }}/images/IMG_0599.jpeg{{ end }}" alt="{{ .Title }}">
|
||||
</div>
|
||||
<div class="flex-1 bg-gray-800 p-6 flex flex-col justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-primary">
|
||||
Article
|
||||
</p>
|
||||
<a href="{{ .RelPermalink }}" class="block mt-2">
|
||||
<p class="text-xl font-semibold text-gray-100">{{ .Title }}</p>
|
||||
<p class="mt-3 text-base text-gray-300">{{ .Summary | truncate 150 }}</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center">
|
||||
<div class="text-sm text-gray-400">
|
||||
<time datetime="{{ .Date }}">{{ .Date.Format "Jan 2, 2006" }}</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-10 flex justify-center">
|
||||
{{ template "_internal/pagination.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
13
layouts/_default/single.html
Normal file
13
layouts/_default/single.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
|
||||
<article class="max-w-3xl mx-auto prose prose-emerald prose-lg prose-invert">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-8">{{ .Title }}</h1>
|
||||
<div class="text-gray-400 mb-10">
|
||||
{{ if .Date }}Published on {{ .Date.Format "January 2, 2006" }}{{ end }}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{{ end }}
|
||||
360
layouts/admin/single.html
Normal file
360
layouts/admin/single.html
Normal file
@@ -0,0 +1,360 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="adminPanel()" x-init="init()">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Login Screen -->
|
||||
<div x-show="!authenticated" class="max-w-md mx-auto">
|
||||
<div class="bg-gray-800 rounded-lg p-8 border border-gray-700">
|
||||
<h1 class="text-3xl font-bold text-gray-100 mb-6 text-center">Admin Login</h1>
|
||||
<form @submit.prevent="adminLogin()" class="space-y-6">
|
||||
<div>
|
||||
<label for="admin-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||
<input type="password" id="admin-password" x-model="adminPassword" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div x-show="loginError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
|
||||
<p x-text="loginError"></p>
|
||||
</div>
|
||||
<button type="submit"
|
||||
:disabled="loginLoading"
|
||||
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
|
||||
<span x-show="!loginLoading">Login</span>
|
||||
<span x-show="loginLoading">Logging in...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Dashboard -->
|
||||
<div x-show="authenticated" x-transition>
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-100">Admin Dashboard</h1>
|
||||
<button @click="logout()" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-gray-700 mb-6 flex">
|
||||
<button @click="activeTab = 'members'; loadPendingMembers()"
|
||||
:class="activeTab === 'members' ? 'border-b-2 border-primary text-primary' : 'text-gray-400 hover:text-gray-300'"
|
||||
class="px-6 py-3 font-medium transition-colors">
|
||||
Pending Members
|
||||
</button>
|
||||
<button @click="activeTab = 'testimonials'; loadTestimonials()"
|
||||
:class="activeTab === 'testimonials' ? 'border-b-2 border-primary text-primary' : 'text-gray-400 hover:text-gray-300'"
|
||||
class="px-6 py-3 font-medium transition-colors">
|
||||
Testimonials
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pending Members Tab -->
|
||||
<div x-show="activeTab === 'members'" x-transition>
|
||||
<div x-show="loadingMembers" class="text-center text-gray-400 py-12">
|
||||
Loading...
|
||||
</div>
|
||||
<div x-show="membersError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200 mb-6">
|
||||
<p x-text="membersError"></p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<template x-for="member in pendingMembers" :key="member.id">
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100" x-text="member.name"></h3>
|
||||
<p class="text-gray-400" x-text="member.email"></p>
|
||||
<p class="text-sm text-gray-500 mt-2" x-text="'Signed up: ' + new Date(member.created_at).toLocaleString()"></p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button @click="approveMember(member.id)"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
|
||||
Approve
|
||||
</button>
|
||||
<button @click="denyMember(member.id)"
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="!loadingMembers && !membersError && pendingMembers.length === 0" class="text-center text-gray-400 py-12">
|
||||
<p>No pending memberships</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials Tab -->
|
||||
<div x-show="activeTab === 'testimonials'" x-transition>
|
||||
<div x-show="loadingTestimonials" class="text-center text-gray-400 py-12">
|
||||
Loading...
|
||||
</div>
|
||||
<div x-show="testimonialsError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200 mb-6">
|
||||
<p x-text="testimonialsError"></p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<template x-for="testimonial in allTestimonials" :key="testimonial.id">
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100" x-text="testimonial.author_name"></h3>
|
||||
<p class="text-sm text-gray-400" x-text="new Date(testimonial.created_at).toLocaleString()"></p>
|
||||
<span x-show="testimonial.approved" class="inline-block mt-2 px-2 py-1 bg-green-600/20 text-green-300 text-xs rounded">
|
||||
Approved
|
||||
</span>
|
||||
<span x-show="!testimonial.approved" class="inline-block mt-2 px-2 py-1 bg-yellow-600/20 text-yellow-300 text-xs rounded">
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button x-show="!testimonial.approved"
|
||||
@click="approveTestimonial(testimonial.id)"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
|
||||
Approve
|
||||
</button>
|
||||
<button @click="deleteTestimonial(testimonial.id)"
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-300 leading-relaxed" x-text="testimonial.content"></p>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="!loadingTestimonials && !testimonialsError && allTestimonials.length === 0" class="text-center text-gray-400 py-12">
|
||||
<p>No testimonials</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const getAdminApiBase = () => {
|
||||
const host = window.location.hostname;
|
||||
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
|
||||
return isLocal ? 'http://' + host + ':5001' : window.location.origin.replace(/\/$/, '');
|
||||
};
|
||||
const ADMIN_API_BASE = getAdminApiBase() + '/api';
|
||||
|
||||
function adminPanel() {
|
||||
return {
|
||||
authenticated: false,
|
||||
adminToken: null,
|
||||
adminPassword: '',
|
||||
loginError: '',
|
||||
loginLoading: false,
|
||||
activeTab: 'members',
|
||||
pendingMembers: [],
|
||||
loadingMembers: false,
|
||||
membersError: '',
|
||||
allTestimonials: [],
|
||||
loadingTestimonials: false,
|
||||
testimonialsError: '',
|
||||
|
||||
init() {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
if (token) {
|
||||
this.adminToken = token;
|
||||
this.authenticated = true;
|
||||
this.loadPendingMembers();
|
||||
}
|
||||
},
|
||||
|
||||
async adminLogin() {
|
||||
this.loginError = '';
|
||||
this.loginLoading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: this.adminPassword })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
this.loginLoading = false;
|
||||
|
||||
if (response.ok) {
|
||||
this.adminToken = data.token;
|
||||
localStorage.setItem('admin_token', data.token);
|
||||
this.authenticated = true;
|
||||
this.adminPassword = '';
|
||||
this.loadPendingMembers();
|
||||
} else {
|
||||
this.loginError = data.error || 'Login failed';
|
||||
}
|
||||
} catch (error) {
|
||||
this.loginLoading = false;
|
||||
this.loginError = 'Login failed. Please try again.';
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('admin_token');
|
||||
this.adminToken = null;
|
||||
this.authenticated = false;
|
||||
},
|
||||
|
||||
async loadPendingMembers() {
|
||||
this.loadingMembers = true;
|
||||
this.membersError = '';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/pending`, {
|
||||
headers: { 'Authorization': `Bearer ${this.adminToken}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
this.loadingMembers = false;
|
||||
|
||||
if (response.ok) {
|
||||
this.pendingMembers = data.members || [];
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
this.logout();
|
||||
this.loginError = 'Session expired. Please login again.';
|
||||
} else {
|
||||
this.membersError = data.error || 'Failed to load members';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.loadingMembers = false;
|
||||
this.membersError = 'Failed to load members';
|
||||
}
|
||||
},
|
||||
|
||||
async approveMember(userId) {
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/approve`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.adminToken}`
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.loadPendingMembers();
|
||||
alert('Member approved successfully');
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
this.logout();
|
||||
} else {
|
||||
alert(data.error || 'Failed to approve member');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to approve member');
|
||||
}
|
||||
},
|
||||
|
||||
async denyMember(userId) {
|
||||
if (!confirm('Are you sure you want to deny this membership?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/deny`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.adminToken}`
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.loadPendingMembers();
|
||||
alert('Member denied');
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
this.logout();
|
||||
} else {
|
||||
alert(data.error || 'Failed to deny member');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to deny member');
|
||||
}
|
||||
},
|
||||
|
||||
async loadTestimonials() {
|
||||
this.loadingTestimonials = true;
|
||||
this.testimonialsError = '';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials`, {
|
||||
headers: { 'Authorization': `Bearer ${this.adminToken}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
this.loadingTestimonials = false;
|
||||
|
||||
if (response.ok) {
|
||||
this.allTestimonials = data.testimonials || [];
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
this.logout();
|
||||
} else {
|
||||
this.testimonialsError = data.error || 'Failed to load testimonials';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.loadingTestimonials = false;
|
||||
this.testimonialsError = 'Failed to load testimonials';
|
||||
}
|
||||
},
|
||||
|
||||
async approveTestimonial(testimonialId) {
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials/${testimonialId}/approve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${this.adminToken}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.loadTestimonials();
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
this.logout();
|
||||
} else {
|
||||
alert(data.error || 'Failed to approve testimonial');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to approve testimonial');
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTestimonial(testimonialId) {
|
||||
if (!confirm('Are you sure you want to delete this testimonial?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials/${testimonialId}/delete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${this.adminToken}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.loadTestimonials();
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
this.logout();
|
||||
} else {
|
||||
alert(data.error || 'Failed to delete testimonial');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to delete testimonial');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
35
layouts/albums/list.html
Normal file
35
layouts/albums/list.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto text-center">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl">Photo Albums</h1>
|
||||
<p class="mt-4 max-w-2xl text-xl text-gray-300 mx-auto">
|
||||
Visual documentation of successful grows and sterile setups.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 max-w-7xl mx-auto grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{{ range .Pages }}
|
||||
<a href="{{ .RelPermalink }}" class="group relative block rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-all transform hover:-translate-y-1">
|
||||
<div class="aspect-[4/3] bg-gray-800">
|
||||
<!-- Cover Image (First image from params or fallback) -->
|
||||
{{ $cover := "/images/IMG_0599.jpeg" }}
|
||||
{{ if .Params.cover }}
|
||||
{{ $cover = .Params.cover }}
|
||||
{{ else if .Params.images }}
|
||||
{{ range first 1 .Params.images }}
|
||||
{{ $cover = .url }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<img src="{{ $cover }}" alt="{{ .Title }}" class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500">
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end p-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-white">{{ .Title }}</h3>
|
||||
<p class="text-gray-200 text-sm mt-1">{{ .Params.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
98
layouts/albums/single.html
Normal file
98
layouts/albums/single.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8"
|
||||
x-data="{
|
||||
lightboxOpen: false,
|
||||
activeImage: '',
|
||||
activeCaption: '',
|
||||
images: [
|
||||
{{ range .Params.images }}
|
||||
{ url: '{{ .url }}', caption: '{{ .caption }}' },
|
||||
{{ end }}
|
||||
],
|
||||
currentIndex: 0,
|
||||
openLightbox(index) {
|
||||
this.currentIndex = index;
|
||||
this.activeImage = this.images[index].url;
|
||||
this.activeCaption = this.images[index].caption;
|
||||
this.lightboxOpen = true;
|
||||
},
|
||||
next() {
|
||||
this.currentIndex = (this.currentIndex + 1) % this.images.length;
|
||||
this.activeImage = this.images[this.currentIndex].url;
|
||||
this.activeCaption = this.images[this.currentIndex].caption;
|
||||
},
|
||||
prev() {
|
||||
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
|
||||
this.activeImage = this.images[this.currentIndex].url;
|
||||
this.activeCaption = this.images[this.currentIndex].caption;
|
||||
}
|
||||
}"
|
||||
@keydown.escape.window="lightboxOpen = false"
|
||||
@keydown.arrow-right.window="next()"
|
||||
@keydown.arrow-left.window="prev()">
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<a href="/albums/" class="text-primary hover:text-emerald-700 font-medium mb-4 inline-block">← Back to Albums</a>
|
||||
<h1 class="text-4xl font-extrabold text-gray-100">{{ .Title }}</h1>
|
||||
<p class="mt-4 text-gray-300">{{ .Description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Masonry Grid -->
|
||||
<div class="columns-1 md:columns-2 lg:columns-3 gap-8 space-y-8">
|
||||
<template x-for="(img, index) in images" :key="index">
|
||||
<div class="break-inside-avoid rounded-xl overflow-hidden shadow-md hover:shadow-xl transition-all cursor-pointer bg-gray-800"
|
||||
@click="openLightbox(index)">
|
||||
|
||||
<template x-if="img.url.toLowerCase().endsWith('.mov') || img.url.toLowerCase().endsWith('.mp4')">
|
||||
<video class="w-full h-auto object-cover" controls preload="metadata">
|
||||
<source :src="img.url">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<template x-if="!(img.url.toLowerCase().endsWith('.mov') || img.url.toLowerCase().endsWith('.mp4'))">
|
||||
<img :src="img.url" :alt="img.caption" class="w-full h-auto">
|
||||
</template>
|
||||
|
||||
<div class="p-4" x-show="img.caption">
|
||||
<p class="text-sm text-gray-600" x-text="img.caption"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox Modal -->
|
||||
<div x-show="lightboxOpen"
|
||||
style="display: none;"
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-sm"
|
||||
x-transition.opacity>
|
||||
|
||||
<!-- Close Button -->
|
||||
<button @click="lightboxOpen = false" class="absolute top-4 right-4 text-white text-4xl font-light hover:text-gray-300 focus:outline-none">×</button>
|
||||
|
||||
<!-- Nav Buttons -->
|
||||
<button @click="prev()" class="absolute left-4 text-white text-4xl font-light hover:text-gray-300 focus:outline-none p-4">‹</button>
|
||||
<button @click="next()" class="absolute right-4 text-white text-4xl font-light hover:text-gray-300 focus:outline-none p-4">›</button>
|
||||
|
||||
<!-- Image/Video Display -->
|
||||
<div class="max-w-5xl max-h-screen p-4 flex flex-col items-center justify-center w-full">
|
||||
|
||||
<template x-if="activeImage.toLowerCase().endsWith('.mov') || activeImage.toLowerCase().endsWith('.mp4')">
|
||||
<video controls autoplay class="max-h-[85vh] max-w-full rounded-lg shadow-2xl">
|
||||
<source :src="activeImage">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<template x-if="!(activeImage.toLowerCase().endsWith('.mov') || activeImage.toLowerCase().endsWith('.mp4'))">
|
||||
<img :src="activeImage" class="max-h-[85vh] max-w-full rounded-lg shadow-2xl object-contain">
|
||||
</template>
|
||||
|
||||
<p x-text="activeCaption" class="text-white mt-4 text-lg font-medium tracking-wide"></p>
|
||||
<p class="text-gray-400 text-sm mt-1" x-text="(currentIndex + 1) + ' / ' + images.length"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
498
layouts/chat/single.html
Normal file
498
layouts/chat/single.html
Normal file
@@ -0,0 +1,498 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-8 px-4 sm:px-6 lg:px-8" x-data="chatInterface()" x-init="init()">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">
|
||||
Chat with Dr. Jones AI
|
||||
</h1>
|
||||
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
Get instant answers about sterile hydroponics, cannabis cultivation, and growing techniques. Drag library files into the chat to ask questions about specific documents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Layout: Sidebar + Chat -->
|
||||
<div class="flex flex-col lg:flex-row gap-6 lg:h-[700px]">
|
||||
<!-- Left Sidebar: Library Files -->
|
||||
<div class="w-full lg:w-80 flex-shrink-0 h-64 lg:h-auto bg-gray-800 rounded-lg shadow-xl border border-gray-700 flex flex-col">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-lg font-bold text-gray-100 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Library Files
|
||||
</h2>
|
||||
<p class="text-xs text-gray-400 mt-1">Drag files into chat to reference them</p>
|
||||
</div>
|
||||
|
||||
<!-- Search/Filter -->
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<input
|
||||
type="text"
|
||||
x-model="fileSearch"
|
||||
placeholder="Search files..."
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Files List -->
|
||||
<div class="flex-1 overflow-y-auto p-2">
|
||||
<template x-if="loadingFiles">
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading files...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loadingFiles && filteredFiles.length === 0">
|
||||
<div class="text-center py-8 text-gray-400 text-sm">
|
||||
No files found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2" x-show="filteredFiles.length > 0">
|
||||
<template x-for="file in filteredFiles" :key="file.file">
|
||||
<div
|
||||
draggable="true"
|
||||
@dragstart="dragStart($event, file)"
|
||||
@dragend="dragEnd($event)"
|
||||
class="p-3 bg-gray-700 hover:bg-gray-600 rounded-lg cursor-move transition-colors border border-transparent hover:border-primary/30"
|
||||
:class="{ 'border-primary': isDragging && draggedFile?.file === file.file }"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-gray-100 text-sm leading-tight" x-text="file.title"></div>
|
||||
<div class="text-xs text-gray-400 mt-1" x-text="file.category"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Chat Container -->
|
||||
<div class="flex-1 bg-gray-800 rounded-lg shadow-xl border border-gray-700 flex flex-col">
|
||||
<!-- Messages Area -->
|
||||
<div
|
||||
class="flex-1 overflow-y-auto p-6 space-y-4"
|
||||
x-ref="messagesContainer"
|
||||
@dragover.prevent="dragOver($event)"
|
||||
@drop.prevent="dropFile($event)"
|
||||
:class="{ 'bg-primary/5 border-2 border-primary border-dashed': isDragOver }"
|
||||
>
|
||||
<!-- Welcome Message -->
|
||||
<template x-if="messages.length === 0">
|
||||
<div class="text-center py-12">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/20 mb-4">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-lg mb-2">Ask me anything about sterile hydroponics!</p>
|
||||
<p class="text-gray-500 text-sm">Drag library files here to ask questions about them</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Messages -->
|
||||
<template x-for="(message, index) in messages" :key="index">
|
||||
<div class="flex" :class="message.role === 'user' ? 'justify-end' : 'justify-start'">
|
||||
<div class="max-w-[80%] rounded-lg px-4 py-3"
|
||||
:class="message.role === 'user'
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-gray-700 text-gray-100'">
|
||||
<div class="flex items-start gap-3">
|
||||
<template x-if="message.role === 'assistant'">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium mb-1" x-show="message.role === 'assistant'">Dr. Jones AI</p>
|
||||
<div class="prose prose-sm prose-invert max-w-none" x-html="formatMessage(message.content)"></div>
|
||||
<!-- Show referenced files -->
|
||||
<template x-if="message.files && message.files.length > 0">
|
||||
<div class="mt-2 pt-2 border-t border-gray-600/50">
|
||||
<div class="text-xs text-gray-400 mb-1">Referenced files:</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="file in message.files" :key="file.file">
|
||||
<span class="text-xs px-2 py-0.5 bg-gray-600/50 rounded" x-text="file.title"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-if="message.role === 'user'">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary/30 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<template x-if="loading">
|
||||
<div class="flex justify-start">
|
||||
<div class="max-w-[80%] rounded-lg px-4 py-3 bg-gray-700 text-gray-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex space-x-1">
|
||||
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0ms"></div>
|
||||
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 150ms"></div>
|
||||
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 300ms"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-400 ml-2">Dr. Jones AI is thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="border-t border-gray-700 p-4">
|
||||
<!-- Selected Files Display -->
|
||||
<div x-show="selectedFiles.length > 0" class="mb-3 pb-3 border-b border-gray-700">
|
||||
<div class="text-xs text-gray-400 mb-2">Files to include in this message:</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="file in selectedFiles" :key="file.file">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1.5 bg-primary/20 text-primary rounded-lg text-xs">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span x-text="file.title"></span>
|
||||
<button @click="removeSelectedFile(file)" class="hover:text-primary/70">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="sendMessage()" class="flex gap-3">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="inputMessage"
|
||||
@keydown.enter.prevent="sendMessage()"
|
||||
:disabled="loading || rateLimited"
|
||||
placeholder="Ask about sterile hydroponics, or drag files here to ask questions about them..."
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<template x-if="rateLimited">
|
||||
<p class="absolute -top-6 left-0 text-xs text-yellow-400">
|
||||
Rate limit: Please wait a moment before sending another message
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!inputMessage.trim() || loading || rateLimited"
|
||||
class="px-6 py-3 bg-primary text-white rounded-lg font-semibold hover:bg-emerald-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<template x-if="!loading">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
<span>Send</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-2 text-center">
|
||||
Powered by Ollama • Responses are AI-generated and should be verified
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="mt-8 bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-xl font-bold text-gray-100 mb-4">About This Chat</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4 text-sm text-gray-300">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-100 mb-2">What I Can Help With</h3>
|
||||
<ul class="space-y-1 list-disc list-inside text-gray-400">
|
||||
<li>Sterile hydroponic protocols</li>
|
||||
<li>Nutrient management (EC, pH)</li>
|
||||
<li>System design and setup</li>
|
||||
<li>Problem diagnosis</li>
|
||||
<li>Questions about library documents</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-100 mb-2">How to Use</h3>
|
||||
<ul class="space-y-1 list-disc list-inside text-gray-400">
|
||||
<li>Drag files from the sidebar into chat</li>
|
||||
<li>Ask questions about specific documents</li>
|
||||
<li>Up to 3 files can be referenced per message</li>
|
||||
<li>Files are processed securely on the server</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function chatInterface() {
|
||||
return {
|
||||
messages: [],
|
||||
inputMessage: '',
|
||||
loading: false,
|
||||
rateLimited: false,
|
||||
lastMessageTime: 0,
|
||||
messageCount: 0,
|
||||
rateLimitWindow: 60000,
|
||||
maxMessagesPerWindow: 10,
|
||||
// File selector state
|
||||
libraryFiles: [],
|
||||
loadingFiles: false,
|
||||
fileSearch: '',
|
||||
selectedFiles: [],
|
||||
// Drag and drop state
|
||||
isDragging: false,
|
||||
draggedFile: null,
|
||||
isDragOver: false,
|
||||
|
||||
get filteredFiles() {
|
||||
if (!this.fileSearch.trim()) {
|
||||
return this.libraryFiles;
|
||||
}
|
||||
const search = this.fileSearch.toLowerCase();
|
||||
return this.libraryFiles.filter(file =>
|
||||
file.title.toLowerCase().includes(search) ||
|
||||
file.category.toLowerCase().includes(search)
|
||||
);
|
||||
},
|
||||
|
||||
init() {
|
||||
this.apiBase = this.getApiBase();
|
||||
// Load conversation history from sessionStorage
|
||||
const saved = sessionStorage.getItem('chatHistory');
|
||||
if (saved) {
|
||||
try {
|
||||
this.messages = JSON.parse(saved);
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
} catch (e) {
|
||||
console.error('Failed to load chat history', e);
|
||||
}
|
||||
}
|
||||
// Load library files
|
||||
this.loadLibraryFiles();
|
||||
},
|
||||
|
||||
getApiBase() {
|
||||
const host = window.location.hostname;
|
||||
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
|
||||
return isLocal ? 'http://localhost:5000' : window.location.origin.replace(/\/$/, '');
|
||||
},
|
||||
|
||||
async loadLibraryFiles() {
|
||||
this.loadingFiles = true;
|
||||
try {
|
||||
const apiUrl = `${this.apiBase}/api/library/files`;
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.libraryFiles = data.files || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load library files:', error);
|
||||
} finally {
|
||||
this.loadingFiles = false;
|
||||
}
|
||||
},
|
||||
|
||||
dragStart(event, file) {
|
||||
this.isDragging = true;
|
||||
this.draggedFile = file;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', JSON.stringify(file));
|
||||
},
|
||||
|
||||
dragEnd(event) {
|
||||
this.isDragging = false;
|
||||
this.draggedFile = null;
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
this.isDragOver = true;
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
},
|
||||
|
||||
dropFile(event) {
|
||||
this.isDragOver = false;
|
||||
try {
|
||||
const fileData = event.dataTransfer.getData('text/plain');
|
||||
if (fileData) {
|
||||
const file = JSON.parse(fileData);
|
||||
this.addSelectedFile(file);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error handling drop:', e);
|
||||
}
|
||||
},
|
||||
|
||||
addSelectedFile(file) {
|
||||
// Limit to 3 files max
|
||||
if (this.selectedFiles.length >= 3) {
|
||||
alert('You can reference up to 3 files at a time.');
|
||||
return;
|
||||
}
|
||||
// Don't add duplicates
|
||||
if (!this.selectedFiles.find(f => f.file === file.file)) {
|
||||
this.selectedFiles.push(file);
|
||||
}
|
||||
},
|
||||
|
||||
removeSelectedFile(file) {
|
||||
this.selectedFiles = this.selectedFiles.filter(f => f.file !== file.file);
|
||||
},
|
||||
|
||||
formatMessage(text) {
|
||||
const safe = this.escapeHTML(text || '');
|
||||
return safe
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/`(.*?)`/g, '<code class="bg-gray-900 px-1 py-0.5 rounded text-primary">$1</code>');
|
||||
},
|
||||
|
||||
escapeHTML(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastMessageTime > this.rateLimitWindow) {
|
||||
this.messageCount = 0;
|
||||
this.lastMessageTime = now;
|
||||
}
|
||||
if (this.messageCount >= this.maxMessagesPerWindow) {
|
||||
this.rateLimited = true;
|
||||
setTimeout(() => {
|
||||
this.rateLimited = false;
|
||||
this.messageCount = 0;
|
||||
}, this.rateLimitWindow - (now - this.lastMessageTime));
|
||||
return false;
|
||||
}
|
||||
this.messageCount++;
|
||||
this.lastMessageTime = now;
|
||||
return true;
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
const message = this.inputMessage.trim();
|
||||
if (!message || this.loading || this.rateLimited) return;
|
||||
|
||||
if (!this.checkRateLimit()) return;
|
||||
|
||||
const safeForDisplay = this.escapeHTML(message);
|
||||
|
||||
// Store selected files for this message
|
||||
const filesForMessage = [...this.selectedFiles];
|
||||
|
||||
// Add user message
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: safeForDisplay,
|
||||
files: filesForMessage.length > 0 ? filesForMessage : undefined
|
||||
});
|
||||
|
||||
this.inputMessage = '';
|
||||
this.selectedFiles = []; // Clear selected files after sending
|
||||
this.loading = true;
|
||||
this.saveHistory();
|
||||
this.scrollToBottom();
|
||||
|
||||
try {
|
||||
const sanitizedMessage = this.sanitizeInput(message);
|
||||
const apiUrl = `${this.apiBase}/api/chat`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: sanitizedMessage,
|
||||
files: filesForMessage.map(f => ({ file: f.file, title: f.title })), // Only send file references
|
||||
history: this.messages.slice(-5)
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: data.response || 'Sorry, I encountered an error. Please try again.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat error:', error);
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: 'Sorry, I\'m having trouble connecting right now. Please try again in a moment.'
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.saveHistory();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
},
|
||||
|
||||
sanitizeInput(input) {
|
||||
return input
|
||||
.replace(/[<>]/g, '')
|
||||
.substring(0, 1000)
|
||||
.trim();
|
||||
},
|
||||
|
||||
saveHistory() {
|
||||
try {
|
||||
sessionStorage.setItem('chatHistory', JSON.stringify(this.messages));
|
||||
} catch (e) {
|
||||
console.error('Failed to save chat history', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
40
layouts/contact/single.html
Normal file
40
layouts/contact/single.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8" x-data="contactPage()" x-init="initContactPage()">
|
||||
<article class="max-w-3xl mx-auto">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-8">{{ .Title }}</h1>
|
||||
<div class="mt-6 prose prose-emerald prose-lg prose-invert max-w-none">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function contactPage() {
|
||||
return {
|
||||
initContactPage() {
|
||||
// Check auth status and show/hide form
|
||||
setTimeout(() => {
|
||||
const authState = window.MembershipAPI.getAuthState();
|
||||
const paywallMessage = document.querySelector('.paywall-message');
|
||||
const paywallContent = document.querySelector('.paywall-content');
|
||||
const membershipOnly = document.querySelectorAll('.membership-only');
|
||||
const guestOnly = document.querySelectorAll('.guest-only');
|
||||
|
||||
if (authState.authenticated) {
|
||||
if (paywallMessage) paywallMessage.style.display = 'none';
|
||||
if (paywallContent) paywallContent.style.display = 'block';
|
||||
membershipOnly.forEach(el => el.style.display = 'block');
|
||||
guestOnly.forEach(el => el.style.display = 'none');
|
||||
} else {
|
||||
if (paywallMessage) paywallMessage.style.display = 'block';
|
||||
if (paywallContent) paywallContent.style.display = 'none';
|
||||
membershipOnly.forEach(el => el.style.display = 'none');
|
||||
guestOnly.forEach(el => el.style.display = 'block');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
308
layouts/earnings/single.html
Normal file
308
layouts/earnings/single.html
Normal file
@@ -0,0 +1,308 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="earningsCalculator()">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">
|
||||
Discover Your Potential Earnings
|
||||
</h1>
|
||||
<p class="text-xl text-gray-300 max-w-3xl mx-auto">
|
||||
See what you could earn with my sterile hydroponics methodology. Based on proven results: <span class="text-primary font-semibold">1.5lb of cannabis from a 2x2.5x7 space</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Calculator Form -->
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-8 mb-8 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-6">Grow Room Specifications</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- CFM Airflow -->
|
||||
<div>
|
||||
<label for="cfm" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Room CFM (Cubic Feet per Minute) Airflow
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="cfm"
|
||||
x-model="cfm"
|
||||
@input="calculate()"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter CFM"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Number of Plants -->
|
||||
<div>
|
||||
<label for="plants" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Number of Plants
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="plants"
|
||||
x-model="plants"
|
||||
@input="calculate()"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter number"
|
||||
min="1"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Number of Lights -->
|
||||
<div>
|
||||
<label for="lights" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Number of Lights
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="lights"
|
||||
x-model="lights"
|
||||
@input="calculate()"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter number"
|
||||
min="1"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Total Wattage -->
|
||||
<div>
|
||||
<label for="wattage" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Total Wattage
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="wattage"
|
||||
x-model="wattage"
|
||||
@input="calculate()"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter watts"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Width -->
|
||||
<div>
|
||||
<label for="width" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Width (feet)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="width"
|
||||
x-model="width"
|
||||
@input="calculate()"
|
||||
step="0.1"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter width"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Height -->
|
||||
<div>
|
||||
<label for="height" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Height (feet)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="height"
|
||||
x-model="height"
|
||||
@input="calculate()"
|
||||
step="0.1"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter height"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Length -->
|
||||
<div>
|
||||
<label for="length" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Length (feet)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="length"
|
||||
x-model="length"
|
||||
@input="calculate()"
|
||||
step="0.1"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
placeholder="Enter length"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Method (Fixed to Hydro) -->
|
||||
<div>
|
||||
<label for="method" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Growing Method
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="method"
|
||||
value="Hydroponics"
|
||||
disabled
|
||||
class="w-full px-4 py-2 bg-gray-600 border border-gray-500 rounded-md text-gray-400 cursor-not-allowed"
|
||||
>
|
||||
<p class="mt-1 text-xs text-gray-400">Always set to Hydroponics</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div x-show="hasResults"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform translate-y-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||
class="space-y-6">
|
||||
|
||||
<!-- Breakdown Table -->
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-6">Your Potential Results</h2>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700">
|
||||
<th class="pb-4 text-sm font-semibold text-gray-300 uppercase">Metric</th>
|
||||
<th class="pb-4 text-sm font-semibold text-gray-300 uppercase text-right">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-gray-200">
|
||||
<tr class="border-b border-gray-700">
|
||||
<td class="py-4 font-medium">Reservoir Size Needed</td>
|
||||
<td class="py-4 text-right">
|
||||
<span class="text-primary font-bold text-lg" x-text="formatNumber(results.waterGallons)"></span>
|
||||
<span class="text-gray-400 ml-2">gallons</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-700">
|
||||
<td class="py-4 font-medium">Total Yield (Grams)</td>
|
||||
<td class="py-4 text-right">
|
||||
<span class="text-primary font-bold text-lg" x-text="formatNumber(results.totalGrams)"></span>
|
||||
<span class="text-gray-400 ml-2">grams</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-700">
|
||||
<td class="py-4 font-medium">Price per Gram</td>
|
||||
<td class="py-4 text-right">
|
||||
<span class="text-primary font-bold text-lg">$10.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-700">
|
||||
<td class="py-4 font-medium">Grow Cycle Duration</td>
|
||||
<td class="py-4 text-right">
|
||||
<span class="text-primary font-bold text-lg">2.5</span>
|
||||
<span class="text-gray-400 ml-2">months</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grand Totals - Exciting Display -->
|
||||
<div class="bg-gradient-to-br from-primary/20 to-emerald-600/20 rounded-lg shadow-xl p-8 border-2 border-primary">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-extrabold text-gray-100 mb-4">
|
||||
🎉 Potential Earnings 🎉
|
||||
</h2>
|
||||
<div class="mb-6">
|
||||
<p class="text-lg text-gray-300 mb-2">Total Revenue</p>
|
||||
<p class="text-6xl font-bold text-primary mb-2" x-text="'$' + formatNumber(results.totalRevenue)"></p>
|
||||
<p class="text-xl text-gray-400" x-text="'From ' + formatNumber(results.totalGrams) + ' grams in just 2.5 months!'"></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-6 bg-gray-800/50 rounded-lg border border-primary/30">
|
||||
<p class="text-2xl font-bold text-gray-100 mb-2">
|
||||
<span class="text-primary">WOAH!</span> You could make that much!
|
||||
</p>
|
||||
<p class="text-gray-300 mt-4">
|
||||
This is what's possible with my sterile hydroponics methodology.
|
||||
<span class="text-primary font-semibold">Ready to make it happen?</span>
|
||||
</p>
|
||||
<a href="/services/" class="inline-block mt-6 px-8 py-3 bg-primary text-white rounded-lg font-semibold hover:bg-emerald-700 transition-colors shadow-lg hover:shadow-xl">
|
||||
Work With Me
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="!hasResults"
|
||||
class="text-center py-12 bg-gray-800 rounded-lg border border-gray-700">
|
||||
<p class="text-gray-400 text-lg">Enter your grow room specifications above to see your potential earnings!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function earningsCalculator() {
|
||||
return {
|
||||
// Input values
|
||||
cfm: '',
|
||||
plants: '',
|
||||
lights: '',
|
||||
wattage: '',
|
||||
width: '',
|
||||
height: '',
|
||||
length: '',
|
||||
|
||||
// Results
|
||||
results: {
|
||||
waterGallons: 0,
|
||||
totalGrams: 0,
|
||||
totalRevenue: 0
|
||||
},
|
||||
|
||||
// Calculation constants
|
||||
// Base ratio: 2x2.5x7 space = 1.5lb
|
||||
// 2 * 2.5 * 7 = 35 cubic feet = 1.5lb = 680.39 grams
|
||||
BASE_CUBIC_FEET: 35, // 2 * 2.5 * 7
|
||||
BASE_GRAMS: 680.39, // 1.5lb in grams
|
||||
GALLONS_PER_PLANT: 0.7,
|
||||
PRICE_PER_GRAM: 10,
|
||||
|
||||
get hasResults() {
|
||||
return this.width > 0 && this.height > 0 && this.length > 0 && this.plants > 0;
|
||||
},
|
||||
|
||||
calculate() {
|
||||
if (!this.hasResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate cubic feet of grow space
|
||||
const cubicFeet = parseFloat(this.width) * parseFloat(this.height) * parseFloat(this.length);
|
||||
|
||||
// Calculate yield based on ratio
|
||||
// If user's space is 35 cubic feet, they get 680.39 grams
|
||||
// Scale proportionally
|
||||
const scaleFactor = cubicFeet / this.BASE_CUBIC_FEET;
|
||||
const totalGrams = this.BASE_GRAMS * scaleFactor;
|
||||
|
||||
// Calculate water needed: 0.7 gallons per plant
|
||||
const waterGallons = parseFloat(this.plants) * this.GALLONS_PER_PLANT;
|
||||
|
||||
// Calculate total revenue
|
||||
const totalRevenue = totalGrams * this.PRICE_PER_GRAM;
|
||||
|
||||
// Update results
|
||||
this.results = {
|
||||
waterGallons: Math.round(waterGallons * 10) / 10, // Round to 1 decimal
|
||||
totalGrams: Math.round(totalGrams * 10) / 10, // Round to 1 decimal
|
||||
totalRevenue: Math.round(totalRevenue * 100) / 100 // Round to 2 decimals
|
||||
};
|
||||
},
|
||||
|
||||
formatNumber(num) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}).format(num);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
121
layouts/index.html
Normal file
121
layouts/index.html
Normal file
@@ -0,0 +1,121 @@
|
||||
{{ define "main" }}
|
||||
<!-- Hero Section -->
|
||||
<div class="relative bg-background overflow-hidden">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="relative z-10 pb-8 bg-background sm:pb-16 md:pb-20 lg:max-w-2xl lg:w-full lg:pb-28 xl:pb-32">
|
||||
<main class="mt-10 mx-auto max-w-7xl px-4 sm:mt-12 sm:px-6 md:mt-16 lg:mt-20 lg:px-8 xl:mt-28">
|
||||
<div class="sm:text-center lg:text-left">
|
||||
<h1 class="text-4xl tracking-tight font-extrabold text-gray-100 sm:text-5xl md:text-6xl">
|
||||
<span class="block xl:inline">Unlock the Power of</span>
|
||||
<span class="block text-primary">Sterile Hydroponics</span>
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-gray-300 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0">
|
||||
Disease-free roots, accelerated growth, and maximum yields. Learn how a clinical approach to gardening transforms your harvest.
|
||||
</p>
|
||||
<div class="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
|
||||
<div class="rounded-md shadow">
|
||||
<a href="/services/" class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-primary hover:bg-emerald-700 md:py-4 md:text-lg md:px-10">
|
||||
Start Mentorship
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-3 sm:mt-0 sm:ml-3">
|
||||
<a href="/library/" class="w-full flex items-center justify-center px-8 py-3 border border-primary text-base font-medium rounded-md text-primary bg-gray-800 hover:bg-gray-700 md:py-4 md:text-lg md:px-10">
|
||||
Free Guide
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:absolute lg:inset-y-0 lg:right-0 lg:w-1/2">
|
||||
<img class="h-56 w-full object-cover sm:h-72 md:h-96 lg:w-full lg:h-full" src="/images/IMG_1767.jpeg" alt="Sterile Hydroponics Setup">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benefits Section -->
|
||||
<div class="py-12 bg-gray-900">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="lg:text-center">
|
||||
<h2 class="text-base text-primary font-semibold tracking-wide uppercase">Why Sterile?</h2>
|
||||
<p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-100 sm:text-4xl">
|
||||
A Better Way to Grow
|
||||
</p>
|
||||
<p class="mt-4 max-w-2xl text-xl text-gray-300 lg:mx-auto">
|
||||
Eliminate pathogens before they start. Our sterile methodology ensures your plants spend energy on growth, not fighting disease.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
<dl class="space-y-10 md:space-y-0 md:grid md:grid-cols-3 md:gap-x-8 md:gap-y-10">
|
||||
<div class="relative">
|
||||
<dt>
|
||||
<div class="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
|
||||
<!-- Icon -->
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ml-16 text-lg leading-6 font-medium text-gray-100">Zero Pathogens</p>
|
||||
</dt>
|
||||
<dd class="mt-2 ml-16 text-base text-gray-300">
|
||||
By maintaining a sterile reservoir, you prevent Pythium (root rot) and other fungal issues completely.
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<dt>
|
||||
<div class="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ml-16 text-lg leading-6 font-medium text-gray-100">Maximum Uptake</p>
|
||||
</dt>
|
||||
<dd class="mt-2 ml-16 text-base text-gray-300">
|
||||
Clean water allows for optimal nutrient absorption without biofilm interference.
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<dt>
|
||||
<div class="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ml-16 text-lg leading-6 font-medium text-gray-100">Clinical Precision</p>
|
||||
</dt>
|
||||
<dd class="mt-2 ml-16 text-base text-gray-300">
|
||||
Data-driven gardening using precise measurements and sterile tools for predictable success.
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Posts (Placeholder) -->
|
||||
<div class="py-12 bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-3xl font-extrabold text-gray-100">Recent from the Journal</h2>
|
||||
<div class="mt-6 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12">
|
||||
{{ range first 3 (where .Site.RegularPages "Section" "blog") }}
|
||||
<div class="flex flex-col rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-48 w-full object-cover" src="{{ with .Params.image }}{{ . }}{{ else }}/images/IMG_0599.jpeg{{ end }}" alt="{{ .Title }}">
|
||||
</div>
|
||||
<div class="flex-1 bg-gray-900 p-6 flex flex-col justify-between">
|
||||
<div class="flex-1">
|
||||
<a href="{{ .RelPermalink }}" class="block mt-2">
|
||||
<p class="text-xl font-semibold text-gray-100">{{ .Title }}</p>
|
||||
<p class="mt-3 text-base text-gray-300">{{ .Summary | truncate 100 }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
86
layouts/library/list.html
Normal file
86
layouts/library/list.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="{ category: 'All' }" x-init="window.MembershipAPI.checkDownloadLimit()">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl">Resource Library</h1>
|
||||
<p class="mt-4 max-w-2xl text-xl text-gray-300 mx-auto">
|
||||
Download guides, checklists, and protocols to upgrade your hydroponic game.
|
||||
<br>
|
||||
<span class="text-sm text-gray-400 italic">For educational purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Paywall Message -->
|
||||
<div class="paywall-message mt-8 bg-gray-800 rounded-lg p-8 border border-gray-700 text-center" style="display: none;">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">Download Limit Reached</h2>
|
||||
<p class="text-gray-300 mb-6">
|
||||
You've used your free download. Become a member for unlimited access to all resources.
|
||||
</p>
|
||||
<a href="/membership/" class="inline-flex items-center px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors">
|
||||
Become a Member
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Download Limit Info -->
|
||||
<div class="guest-only mt-6 bg-blue-900/30 border border-blue-700 rounded-lg p-4 text-center" style="display: block;">
|
||||
<p class="text-blue-200">
|
||||
<span class="font-semibold">Free users:</span> 1 download allowed.
|
||||
<a href="/membership/" class="underline hover:text-blue-100">Sign up for membership</a> for unlimited downloads.
|
||||
</p>
|
||||
</div>
|
||||
<div class="membership-only mt-6 bg-green-900/30 border border-green-700 rounded-lg p-4 text-center" style="display: none;">
|
||||
<p class="text-green-200">
|
||||
<span class="font-semibold">Member:</span> Unlimited downloads available.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mt-10 flex justify-center flex-wrap gap-2">
|
||||
{{ $categories := slice "All" }}
|
||||
{{ range .Site.Data.library }}
|
||||
{{ $categories = $categories | append .category }}
|
||||
{{ end }}
|
||||
{{ range ($categories | uniq) }}
|
||||
<button
|
||||
@click="category = '{{ . }}'"
|
||||
:class="{ 'bg-primary text-white': category === '{{ . }}', 'bg-gray-800 text-gray-300 hover:bg-gray-700': category !== '{{ . }}' }"
|
||||
class="px-4 py-2 rounded-full text-sm font-medium shadow-sm transition-colors border border-gray-700">
|
||||
{{ . }}
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div class="paywall-content mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{{ range .Site.Data.library }}
|
||||
<div x-show="category === 'All' || category === '{{ .category }}'"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
class="bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow overflow-hidden flex flex-col border border-gray-700">
|
||||
<div class="p-6 flex-1">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/20 text-primary">
|
||||
{{ .category }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">{{ .date }}</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<svg class="h-8 w-8 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-100">{{ .title }}</h3>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-gray-300">{{ .description }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 px-6 py-4 border-t border-gray-700">
|
||||
<a href="{{ .file }}" download data-file-path="{{ .file }}" class="download-link flex items-center justify-center w-full px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-primary bg-white hover:bg-gray-50 border-primary transition-colors">
|
||||
Download {{ .filename }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
221
layouts/membership/single.html
Normal file
221
layouts/membership/single.html
Normal file
@@ -0,0 +1,221 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="membershipPage()">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">Become a Member</h1>
|
||||
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
Unlock exclusive access to premium content, unlimited downloads, and direct communication with Dr. Jones.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Benefits Section -->
|
||||
<div class="bg-gray-800 rounded-lg p-8 mb-8 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-6">Member Benefits</h2>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100 mb-2">Unlimited Downloads</h3>
|
||||
<p class="text-gray-300">Access and download all resources from the library without limits.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100 mb-2">Direct Email Access</h3>
|
||||
<p class="text-gray-300">Get my direct email address and submit contact forms for personalized help.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100 mb-2">Member Testimonials</h3>
|
||||
<p class="text-gray-300">Read and share success stories from other growers using the sterile method.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100 mb-2">Priority Support</h3>
|
||||
<p class="text-gray-300">Members get priority responses and access to exclusive content updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buy Me a Coffee Section -->
|
||||
<div class="bg-gradient-to-r from-[#FFDD00] to-[#FFCC00] rounded-lg p-8 mb-8 text-center">
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-4">Support the Method</h2>
|
||||
<p class="text-lg text-gray-800 mb-6">
|
||||
Membership requires a $5 minimum donation via Buy Me a Coffee. This helps support continued development of sterile hydroponic protocols and resources.
|
||||
</p>
|
||||
<a href="https://www.buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener noreferrer"
|
||||
class="inline-flex items-center px-8 py-4 bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-bold text-lg transition-colors shadow-lg">
|
||||
<svg class="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.18 6.78 7.29 3.96.12 7.22-3.06 7.22-7v-1h.5c1.38 0 2.5-1.12 2.5-2.5S19.88 3 18.5 3zM16 5v3H6V5h10zm2.5 5H18V5h.5c.28 0 .5.22.5.5s-.22.5-.5.5zM4 19h16v2H4v-2z"/>
|
||||
</svg>
|
||||
Donate $5 Minimum
|
||||
</a>
|
||||
<p class="text-sm text-gray-700 mt-4 italic">After completing your donation, return here to sign up for membership.</p>
|
||||
</div>
|
||||
|
||||
<!-- Login/Register Tabs -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div class="border-b border-gray-700 flex">
|
||||
<button @click="activeTab = 'login'"
|
||||
:class="activeTab === 'login' ? 'bg-primary text-white' : 'text-gray-300 hover:text-gray-100'"
|
||||
class="flex-1 px-6 py-4 font-medium transition-colors">
|
||||
Login
|
||||
</button>
|
||||
<button @click="activeTab = 'register'"
|
||||
:class="activeTab === 'register' ? 'bg-primary text-white' : 'text-gray-300 hover:text-gray-100'"
|
||||
class="flex-1 px-6 py-4 font-medium transition-colors">
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-8">
|
||||
<!-- Login Form -->
|
||||
<div x-show="activeTab === 'login'" x-transition>
|
||||
<form @submit.prevent="handleLogin()" class="space-y-6">
|
||||
<div>
|
||||
<label for="login-email" class="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
||||
<input type="email" id="login-email" x-model="loginEmail" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="login-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||
<input type="password" id="login-password" x-model="loginPassword" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div x-show="loginError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
|
||||
<p x-text="loginError"></p>
|
||||
</div>
|
||||
<button type="submit"
|
||||
:disabled="loginLoading"
|
||||
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
|
||||
<span x-show="!loginLoading">Login</span>
|
||||
<span x-show="loginLoading">Logging in...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<div x-show="activeTab === 'register'" x-transition>
|
||||
<form @submit.prevent="handleRegister()" class="space-y-6">
|
||||
<div>
|
||||
<label for="register-name" class="block text-sm font-medium text-gray-300 mb-2">Full Name</label>
|
||||
<input type="text" id="register-name" x-model="registerName" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="register-email" class="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
||||
<input type="email" id="register-email" x-model="registerEmail" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="register-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||
<input type="password" id="register-password" x-model="registerPassword" required minlength="8"
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<p class="mt-1 text-sm text-gray-400">Must be at least 8 characters</p>
|
||||
</div>
|
||||
<div x-show="registerError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
|
||||
<p x-text="registerError"></p>
|
||||
</div>
|
||||
<div x-show="registerSuccess" class="bg-green-900/50 border border-green-700 rounded-lg p-4 text-green-200">
|
||||
<p x-text="registerSuccess"></p>
|
||||
</div>
|
||||
<button type="submit"
|
||||
:disabled="registerLoading"
|
||||
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
|
||||
<span x-show="!registerLoading">Sign Up</span>
|
||||
<span x-show="registerLoading">Creating account...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div x-show="statusMessage" class="mt-6 p-4 rounded-lg"
|
||||
:class="statusMessageType === 'success' ? 'bg-green-900/50 border border-green-700 text-green-200' : 'bg-blue-900/50 border border-blue-700 text-blue-200'">
|
||||
<p x-text="statusMessage"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function membershipPage() {
|
||||
return {
|
||||
activeTab: 'login',
|
||||
loginEmail: '',
|
||||
loginPassword: '',
|
||||
loginError: '',
|
||||
loginLoading: false,
|
||||
registerName: '',
|
||||
registerEmail: '',
|
||||
registerPassword: '',
|
||||
registerError: '',
|
||||
registerSuccess: '',
|
||||
registerLoading: false,
|
||||
statusMessage: '',
|
||||
statusMessageType: 'info',
|
||||
|
||||
async handleLogin() {
|
||||
this.loginError = '';
|
||||
this.loginLoading = true;
|
||||
|
||||
const result = await window.MembershipAPI.login(this.loginEmail, this.loginPassword);
|
||||
|
||||
this.loginLoading = false;
|
||||
|
||||
if (result.success) {
|
||||
this.statusMessage = 'Login successful! Redirecting...';
|
||||
this.statusMessageType = 'success';
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1500);
|
||||
} else {
|
||||
this.loginError = result.error || 'Login failed';
|
||||
}
|
||||
},
|
||||
|
||||
async handleRegister() {
|
||||
this.registerError = '';
|
||||
this.registerSuccess = '';
|
||||
this.registerLoading = true;
|
||||
|
||||
const result = await window.MembershipAPI.register(this.registerEmail, this.registerPassword, this.registerName);
|
||||
|
||||
this.registerLoading = false;
|
||||
|
||||
if (result.success) {
|
||||
this.registerSuccess = result.message || 'Registration successful! Your membership is pending approval. You will receive an email when approved.';
|
||||
this.registerEmail = '';
|
||||
this.registerPassword = '';
|
||||
this.registerName = '';
|
||||
} else {
|
||||
this.registerError = result.error || 'Registration failed';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
34
layouts/services/list.html
Normal file
34
layouts/services/list.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto text-center">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl">Work With Me</h1>
|
||||
<p class="mt-4 max-w-2xl text-xl text-gray-300 mx-auto">
|
||||
Professional guidance to fast-track your success.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-16 grid gap-8 lg:grid-cols-3">
|
||||
{{ range .Pages }}
|
||||
<div class="relative bg-gray-800 border border-gray-700 rounded-2xl shadow-sm flex flex-col p-8 hover:shadow-lg transition-shadow">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-semibold text-gray-100">{{ .Title }}</h3>
|
||||
<p class="mt-4 flex items-baseline text-gray-100">
|
||||
<span class="text-4xl font-extrabold tracking-tight">{{ .Params.price }}</span>
|
||||
{{ if .Params.price_period }}<span class="ml-1 text-xl font-semibold text-gray-400">{{ .Params.price_period }}</span>{{ end }}
|
||||
</p>
|
||||
<div class="mt-6 text-gray-300 space-y-4 prose prose-emerald">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<a href="{{ .Params.foxy_link | default "#" }}"
|
||||
class="w-full bg-primary border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white hover:bg-emerald-700 md:text-lg transition-colors">
|
||||
{{ .Params.cta | default "Book Session" }}
|
||||
</a>
|
||||
<p class="mt-4 text-xs text-gray-400 text-center">Secured by Stripe</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
159
layouts/testimonials/single.html
Normal file
159
layouts/testimonials/single.html
Normal file
@@ -0,0 +1,159 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="testimonialsPage()" x-init="initTestimonials()">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">Member Testimonials</h1>
|
||||
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
Real stories from growers who have transformed their gardens using the sterile hydroponic method.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Paywall Message -->
|
||||
<div class="paywall-message bg-gray-800 rounded-lg p-8 border border-gray-700 text-center mb-8" style="display: none;">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">Members Only Content</h2>
|
||||
<p class="text-gray-300 mb-6">
|
||||
Testimonials are available exclusively to members. Sign up to read success stories and share your own experience.
|
||||
</p>
|
||||
<a href="/membership/" class="inline-flex items-center px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors">
|
||||
Become a Member
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials Content -->
|
||||
<div class="paywall-content">
|
||||
<!-- Submit Testimonial Form (Members Only) -->
|
||||
<div class="membership-only bg-gray-800 rounded-lg p-8 mb-8 border border-gray-700" style="display: none;">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-6">Share Your Story</h2>
|
||||
<form @submit.prevent="submitTestimonial()" class="space-y-6">
|
||||
<div>
|
||||
<label for="author-name" class="block text-sm font-medium text-gray-300 mb-2">Your Name</label>
|
||||
<input type="text" id="author-name" x-model="authorName" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="testimonial-content" class="block text-sm font-medium text-gray-300 mb-2">Your Testimonial</label>
|
||||
<textarea id="testimonial-content" x-model="testimonialContent" required rows="6"
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent"></textarea>
|
||||
</div>
|
||||
<div x-show="submitError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
|
||||
<p x-text="submitError"></p>
|
||||
</div>
|
||||
<div x-show="submitSuccess" class="bg-green-900/50 border border-green-700 rounded-lg p-4 text-green-200">
|
||||
<p x-text="submitSuccess"></p>
|
||||
</div>
|
||||
<button type="submit"
|
||||
:disabled="submitLoading"
|
||||
class="px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
|
||||
<span x-show="!submitLoading">Submit Testimonial</span>
|
||||
<span x-show="submitLoading">Submitting...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials List -->
|
||||
<div class="space-y-6">
|
||||
<div x-show="loading" class="text-center text-gray-400 py-12">
|
||||
Loading testimonials...
|
||||
</div>
|
||||
<div x-show="error" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
|
||||
<p x-text="error"></p>
|
||||
</div>
|
||||
<template x-for="testimonial in testimonials" :key="testimonial.id">
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-100" x-text="testimonial.author_name"></h3>
|
||||
<p class="text-sm text-gray-400" x-text="new Date(testimonial.created_at).toLocaleDateString()"></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-300 leading-relaxed" x-text="testimonial.content"></p>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="!loading && !error && testimonials.length === 0" class="text-center text-gray-400 py-12">
|
||||
<p>No testimonials yet. Be the first to share your success story!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function testimonialsPage() {
|
||||
return {
|
||||
testimonials: [],
|
||||
loading: true,
|
||||
error: '',
|
||||
authorName: '',
|
||||
testimonialContent: '',
|
||||
submitError: '',
|
||||
submitSuccess: '',
|
||||
submitLoading: false,
|
||||
|
||||
initTestimonials() {
|
||||
// Check auth and show/hide paywall
|
||||
const authState = window.MembershipAPI.getAuthState();
|
||||
const paywallMessage = document.querySelector('.paywall-message');
|
||||
const paywallContent = document.querySelector('.paywall-content');
|
||||
const membershipOnly = document.querySelectorAll('.membership-only');
|
||||
|
||||
if (!authState.authenticated) {
|
||||
if (paywallMessage) paywallMessage.style.display = 'block';
|
||||
if (paywallContent) paywallContent.style.display = 'none';
|
||||
membershipOnly.forEach(el => el.style.display = 'none');
|
||||
this.loading = false;
|
||||
} else {
|
||||
if (paywallMessage) paywallMessage.style.display = 'none';
|
||||
if (paywallContent) paywallContent.style.display = 'block';
|
||||
membershipOnly.forEach(el => el.style.display = 'block');
|
||||
this.loadTestimonials();
|
||||
}
|
||||
},
|
||||
|
||||
async loadTestimonials() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
const authState = window.MembershipAPI.getAuthState();
|
||||
if (!authState.authenticated) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await window.MembershipAPI.loadTestimonials();
|
||||
this.loading = false;
|
||||
|
||||
if (result.success) {
|
||||
this.testimonials = result.testimonials || [];
|
||||
} else {
|
||||
this.error = result.error || 'Failed to load testimonials';
|
||||
}
|
||||
},
|
||||
|
||||
async submitTestimonial() {
|
||||
if (!this.authorName || !this.testimonialContent) {
|
||||
this.submitError = 'Please fill in all fields';
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitError = '';
|
||||
this.submitSuccess = '';
|
||||
this.submitLoading = true;
|
||||
|
||||
const result = await window.MembershipAPI.submitTestimonial(this.testimonialContent, this.authorName);
|
||||
this.submitLoading = false;
|
||||
|
||||
if (result.success) {
|
||||
this.submitSuccess = result.message || 'Testimonial submitted successfully!';
|
||||
this.authorName = '';
|
||||
this.testimonialContent = '';
|
||||
// Reload testimonials after a delay
|
||||
setTimeout(() => this.loadTestimonials(), 2000);
|
||||
} else {
|
||||
this.submitError = result.error || 'Failed to submit testimonial';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
628
library.yaml.backup
Normal file
628
library.yaml.backup
Normal file
@@ -0,0 +1,628 @@
|
||||
- title: HYDROPONICS GARDEN - The Complete Diy Guide For Beginners To Learn How To
|
||||
Build A System To Grow Plants
|
||||
filename: HYDROPONICS GARDEN - The Complete Diy Guide For Beginners To Learn How
|
||||
To Build A System To Grow Plants.pdf
|
||||
description: 'Educational resource: HYDROPONICS GARDEN - The Complete Diy Guide
|
||||
For Beginners To Learn How To Build A System To Grow Plants'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/HYDROPONICS GARDEN - The Complete Diy Guide For Beginners To Learn
|
||||
How To Build A System To Grow Plants.pdf
|
||||
- title: Tammi Sweet - Beginner's Guide to Growing Cannabis and Making Your Own Healing
|
||||
Remedies Learn about the Plant's Medicinal Properties Grow O
|
||||
filename: Tammi Sweet - Beginner's Guide to Growing Cannabis and Making Your Own
|
||||
Healing Remedies_ Learn about the Plant's Medicinal Properties_ Grow O - libgen.li.epub
|
||||
description: 'Educational resource: Tammi Sweet - Beginner''s Guide to Growing Cannabis
|
||||
and Making Your Own Healing Remedies Learn about the Plant''s Medicinal Properties Grow
|
||||
O'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Tammi Sweet - Beginner's Guide to Growing Cannabis and Making Your
|
||||
Own Healing Remedies_ Learn about the Plant's Medicinal Properties_ Grow O - libgen.li.epub
|
||||
- title: '[Using the Sea of Green Process] Paul Wright - Totally organic hydroponics
|
||||
(2004, Tin Alley Productions)'
|
||||
filename: '[Using the Sea of Green Process] Paul Wright - Totally organic hydroponics
|
||||
(2004, Tin Alley Productions) - libgen.li.pdf'
|
||||
description: 'Educational resource: [Using the Sea of Green Process] Paul Wright
|
||||
- Totally organic hydroponics (2004, Tin Alley Productions)'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/[Using the Sea of Green Process] Paul Wright - Totally organic hydroponics
|
||||
(2004, Tin Alley Productions) - libgen.li.pdf
|
||||
- title: Cannabis Collection - PBS - Clearing the Smoke- The Science of Cannabis
|
||||
filename: Cannabis Collection - PBS - Clearing the Smoke- The Science of Cannabis.mp4
|
||||
description: 'Educational resource: Cannabis Collection - PBS - Clearing the Smoke-
|
||||
The Science of Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis Collection - PBS - Clearing the Smoke- The Science of Cannabis.mp4
|
||||
- title: Amateur Gardening Easy Guide To - Gardening For Beginners 2024
|
||||
filename: Amateur Gardening Easy Guide To - Gardening For Beginners 2024.pdf
|
||||
description: 'Educational resource: Amateur Gardening Easy Guide To - Gardening
|
||||
For Beginners 2024'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Amateur Gardening Easy Guide To - Gardening For Beginners 2024.pdf
|
||||
- title: Encyclopedia of Garden Plants for Every Location - Featuring More Than 3,000
|
||||
Plants By DK
|
||||
filename: Encyclopedia of Garden Plants for Every Location - Featuring More Than
|
||||
3,000 Plants By DK.pdf
|
||||
description: 'Educational resource: Encyclopedia of Garden Plants for Every Location
|
||||
- Featuring More Than 3,000 Plants By DK'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/Encyclopedia of Garden Plants for Every Location - Featuring More
|
||||
Than 3,000 Plants By DK.pdf
|
||||
- title: Cannabis - A Complete Guide (2016)
|
||||
filename: Cannabis - A Complete Guide (2016).pdf
|
||||
description: 'Educational resource: Cannabis - A Complete Guide (2016)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cannabis - A Complete Guide (2016).pdf
|
||||
- title: GUIDE TO HYDROPONICS By James Shelby PH.D
|
||||
filename: GUIDE TO HYDROPONICS By James Shelby PH.D.epub
|
||||
description: 'Educational resource: GUIDE TO HYDROPONICS By James Shelby PH.D'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/GUIDE TO HYDROPONICS By James Shelby PH.D.epub
|
||||
- title: Ed Rosenthal David Downs - Marijuana Harvest How to Maximize Quality and
|
||||
Yield in Your Cannabis Garden (2017, Quick American Archives)
|
||||
filename: Ed Rosenthal_ David Downs - Marijuana Harvest_ How to Maximize Quality
|
||||
and Yield in Your Cannabis Garden (2017, Quick American Archives) - libgen.li.epub
|
||||
description: 'Educational resource: Ed Rosenthal David Downs - Marijuana Harvest How
|
||||
to Maximize Quality and Yield in Your Cannabis Garden (2017, Quick American Archives)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Ed Rosenthal_ David Downs - Marijuana Harvest_ How to Maximize Quality
|
||||
and Yield in Your Cannabis Garden (2017, Quick American Archives) - libgen.li.epub
|
||||
- title: Ecology of Plants, The - Jessica Gurevitch, Samuel M. Scheiner, Gordon A.
|
||||
Fox
|
||||
filename: Ecology of Plants, The - Jessica Gurevitch, Samuel M. Scheiner, Gordon
|
||||
A. Fox.pdf
|
||||
description: 'Educational resource: Ecology of Plants, The - Jessica Gurevitch,
|
||||
Samuel M. Scheiner, Gordon A. Fox'
|
||||
category: Science
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Jessica Gurevitch, Samuel M. Scheiner, Gordon A. Fox/Ecology
|
||||
of Plants, The/Ecology of Plants, The - Jessica Gurevitch, Samuel M. Scheiner,
|
||||
Gordon A. Fox.pdf
|
||||
- title: Hydroponic Food Production - A Definitive Guid Commercial Hydroponic Grower
|
||||
- Howard M. Resh
|
||||
filename: Hydroponic Food Production - A Definitive Guid Commercial Hydroponic Grower
|
||||
- Howard M. Resh.pdf
|
||||
description: 'Educational resource: Hydroponic Food Production - A Definitive Guid
|
||||
Commercial Hydroponic Grower - Howard M. Resh'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Resh,rd M_/Hydroponic Food Production - A Definitive Grdener
|
||||
and the Commercial Hydroponic Grower/Hydroponic Food Production - A Definitive
|
||||
Guid Commercial Hydroponic Grower - Howard M. Resh.pdf
|
||||
- title: GROW GREAT WEED Personal & Medical Marijuana Indoor Outdoor Grower Big Bud
|
||||
Bible - Dale Denton
|
||||
filename: GROW GREAT WEED_ Personal & Medical Marijuana Indoor_Outdoor Grower Big
|
||||
Bud Bible - Dale Denton.pdf
|
||||
description: 'Educational resource: GROW GREAT WEED Personal & Medical Marijuana
|
||||
Indoor Outdoor Grower Big Bud Bible - Dale Denton'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Denton, Dale/GROW GREAT WEED_ Personal & Medical Marijuana Indoor_Outdoor
|
||||
Grower Big Bud Bible/GROW GREAT WEED_ Personal & Medical Marijuana Indoor_Outdoor
|
||||
Grower Big Bud Bible - Dale Denton.pdf
|
||||
- title: Scanned Image - drjones
|
||||
filename: Scanned Image - drjones.pdf
|
||||
description: 'Educational resource: Scanned Image - drjones'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/drjones/Scanned Image/Scanned Image - drjones.pdf
|
||||
- title: 100 Best Cannabis Strains - Michael Blood
|
||||
filename: 100 Best Cannabis Strains - Michael Blood.pdf
|
||||
description: 'Educational resource: 100 Best Cannabis Strains - Michael Blood'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Blood, Michael/100 Best Cannabis Strains/100 Best Cannabis Strains
|
||||
- Michael Blood.pdf
|
||||
- title: downmagaz.net - downmagaz.net
|
||||
filename: downmagaz.net - downmagaz.net.pdf
|
||||
description: 'Educational resource: downmagaz.net - downmagaz.net'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/downmagaz.net/downmagaz.net/downmagaz.net - downmagaz.net.pdf
|
||||
- title: Cannabis Grow Bible, The - Greg Green
|
||||
filename: Cannabis Grow Bible, The - Greg Green.pdf
|
||||
description: 'Educational resource: Cannabis Grow Bible, The - Greg Green'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Green, Greg/Cannabis Grow Bible, The/Cannabis Grow Bible, The
|
||||
- Greg Green.pdf
|
||||
- title: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang - Unknown
|
||||
filename: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang - Unknown.pdf
|
||||
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases by
|
||||
Shouhua Wang - Unknown'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Unknown/Diagnosing Hemp and Cannabis Crop Diseases by Shouhua
|
||||
Wang/Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang - Unknown.pdf
|
||||
- title: GUIDE TO HYDROPONICS - James Shelby PH.D
|
||||
filename: GUIDE TO HYDROPONICS - James Shelby PH.D.pdf
|
||||
description: 'Educational resource: GUIDE TO HYDROPONICS - James Shelby PH.D'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Shelby PH.D, James/GUIDE TO HYDROPONICS/GUIDE TO HYDROPONICS
|
||||
- James Shelby PH.D.pdf
|
||||
- title: HYDROPONICS GARDEN - Water, Tyler
|
||||
filename: HYDROPONICS GARDEN - Water, Tyler.pdf
|
||||
description: 'Educational resource: HYDROPONICS GARDEN - Water, Tyler'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Water, Tyler/HYDROPONICS GARDEN/HYDROPONICS GARDEN - Water,
|
||||
Tyler.pdf
|
||||
- title: How to Grow Marijuana 3 Books in 1 - The Complality Weed Indoors and Outdoors
|
||||
- Whistler, Tom
|
||||
filename: How to Grow Marijuana_ 3 Books in 1 - The Complality Weed Indoors and
|
||||
Outdoors - Whistler, Tom.pdf
|
||||
description: 'Educational resource: How to Grow Marijuana 3 Books in 1 - The Complality
|
||||
Weed Indoors and Outdoors - Whistler, Tom'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Whis Tom/How to Grow Marijuana_ 3 Books in 1 - The Cowing Top-Quality
|
||||
Weed Indoors and Outdoors/How to Grow Marijuana_ 3 Books in 1 - The Complality
|
||||
Weed Indoors and Outdoors - Whistler, Tom.pdf
|
||||
- title: Hydroponics Hydroponics Gardening Guide - frg, horticulture, Cannabis) -
|
||||
Thatcher, Thomas
|
||||
filename: Hydroponics_ Hydroponics Gardening Guide - frg, horticulture, Cannabis)
|
||||
- Thatcher, Thomas.pdf
|
||||
description: 'Educational resource: Hydroponics Hydroponics Gardening Guide - frg,
|
||||
horticulture, Cannabis) - Thatcher, Thomas'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Thatchomas/Hydroponics_ Hydroponics Gardening Guide - fresteading,
|
||||
Gardening, horticulture, Cannabis)/Hydroponics_ Hydroponics Gardening Guide -
|
||||
frg, horticulture, Cannabis) - Thatcher, Thomas.pdf
|
||||
- title: Jesse Ventura's Marijuana Manifesto - Jesse Ventura
|
||||
filename: Jesse Ventura's Marijuana Manifesto - Jesse Ventura.pdf
|
||||
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto - Jesse
|
||||
Ventura'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Ventura, Jesse/Jesse Ventura's Marijuana Manifesto/Jesse Ventura's
|
||||
Marijuana Manifesto - Jesse Ventura.pdf
|
||||
- title: True Living Organics - The Rev
|
||||
filename: True Living Organics - The Rev.pdf
|
||||
description: 'Educational resource: True Living Organics - The Rev'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Rev, The/True Living Organics/True Living Organics - The Rev.pdf
|
||||
- title: DIY Home Hydroponics For Beginners The Essentiao Turn Your Backyard Into
|
||||
A Farm - Sally R. Ball (1)
|
||||
filename: DIY Home Hydroponics For Beginners_ The Essentiao Turn Your Backyard Into
|
||||
A Farm - Sally R. Ball (1).pdf
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners The Essentiao
|
||||
Turn Your Backyard Into A Farm - Sally R. Ball (1)'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Ball, lly R_/DIY Home Hydroponics For Beginners_ The Guide
|
||||
To Turn Your Backyard Into A Farm/DIY Home Hydroponics For Beginners_ The Essentiao
|
||||
Turn Your Backyard Into A Farm - Sally R. Ball (1).pdf
|
||||
- title: DIY Home Hydroponics For Beginners The Essentiao Turn Your Backyard Into
|
||||
A Farm - Sally R. Ball
|
||||
filename: DIY Home Hydroponics For Beginners_ The Essentiao Turn Your Backyard Into
|
||||
A Farm - Sally R. Ball.pdf
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners The Essentiao
|
||||
Turn Your Backyard Into A Farm - Sally R. Ball'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Ball, lly R_/DIY Home Hydroponics For Beginners_ The Guide
|
||||
To Turn Your Backyard Into A Farm/DIY Home Hydroponics For Beginners_ The Essentiao
|
||||
Turn Your Backyard Into A Farm - Sally R. Ball.pdf
|
||||
- title: What a Plant Knows - Daniel Chamovitz
|
||||
filename: What a Plant Knows - Daniel Chamovitz.pdf
|
||||
description: 'Educational resource: What a Plant Knows - Daniel Chamovitz'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/pdfs/Chamovitz, Daniel/What a Plant Knows/What a Plant Knows - Daniel
|
||||
Chamovitz.pdf
|
||||
- title: Emmanuel S. Onaivi - The Biology of Marijuana From Gene to Behavior (2002,
|
||||
CRC Press)
|
||||
filename: Emmanuel S. Onaivi - The Biology of Marijuana_ From Gene to Behavior (2002,
|
||||
CRC Press) - libgen.li.pdf
|
||||
description: 'Educational resource: Emmanuel S. Onaivi - The Biology of Marijuana From
|
||||
Gene to Behavior (2002, CRC Press)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Emmanuel S. Onaivi - The Biology of Marijuana_ From Gene to Behavior
|
||||
(2002, CRC Press) - libgen.li.pdf
|
||||
- title: Greg Green - The Cannabis Breeder's Bible The Definitive Guide to Marijuana
|
||||
Genetics, Cannabis Botany and Creating Strains for the Seed Mark (2005, Green
|
||||
Candy Press)
|
||||
filename: Greg Green - The Cannabis Breeder's Bible_ The Definitive Guide to Marijuana
|
||||
Genetics, Cannabis Botany and Creating Strains for the Seed Mark (2005, Green
|
||||
Candy Press) - libgen.li.epub
|
||||
description: 'Educational resource: Greg Green - The Cannabis Breeder''s Bible The
|
||||
Definitive Guide to Marijuana Genetics, Cannabis Botany and Creating Strains for
|
||||
the Seed Mark (2005, Green Candy Press)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Greg Green - The Cannabis Breeder's Bible_ The Definitive Guide to
|
||||
Marijuana Genetics, Cannabis Botany and Creating Strains for the Seed Mark (2005,
|
||||
Green Candy Press) - libgen.li.epub
|
||||
- title: '[20th Century Alchemist] D. Gold (David Hoye) - Cannabis Alchemy The Art
|
||||
of Modern Hashmaking (1993, Ronin Publishing)'
|
||||
filename: '[20th Century Alchemist] D. Gold (David Hoye) - Cannabis Alchemy_ The
|
||||
Art of Modern Hashmaking (1993, Ronin Publishing) - libgen.li.pdf'
|
||||
description: 'Educational resource: [20th Century Alchemist] D. Gold (David Hoye)
|
||||
- Cannabis Alchemy The Art of Modern Hashmaking (1993, Ronin Publishing)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/[20th Century Alchemist] D. Gold (David Hoye) - Cannabis Alchemy_
|
||||
The Art of Modern Hashmaking (1993, Ronin Publishing) - libgen.li.pdf
|
||||
- title: Marijuana Botany - An Advanced Study - The Propagation and Breeding of Distinctive
|
||||
Cannabis
|
||||
filename: Marijuana Botany - An Advanced Study - The Propagation and Breeding of
|
||||
Distinctive Cannabis.pdf
|
||||
description: 'Educational resource: Marijuana Botany - An Advanced Study - The Propagation
|
||||
and Breeding of Distinctive Cannabis'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Botany - An Advanced Study - The Propagation and Breeding
|
||||
of Distinctive Cannabis.pdf
|
||||
- title: 'DIY Home Hydroponics For Beginners '
|
||||
filename: DIY Home Hydroponics For Beginners .epub
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners '
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Home Hydroponics For Beginners .epub
|
||||
- title: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang
|
||||
filename: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang.pdf
|
||||
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases by
|
||||
Shouhua Wang'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang.pdf
|
||||
- title: '[Handbooks in psychopharmacology] Pertwee, Roger G - Handbook of cannabis
|
||||
(2016, Oxford University Press)'
|
||||
filename: '[Handbooks in psychopharmacology] Pertwee, Roger G - Handbook of cannabis
|
||||
(2016, Oxford University Press) - libgen.li.pdf'
|
||||
description: 'Educational resource: [Handbooks in psychopharmacology] Pertwee, Roger
|
||||
G - Handbook of cannabis (2016, Oxford University Press)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/[Handbooks in psychopharmacology] Pertwee, Roger G - Handbook of
|
||||
cannabis (2016, Oxford University Press) - libgen.li.pdf
|
||||
- title: T. Asao - Hydroponics -A Standard Methodology for Plant Biological Researches-Intech
|
||||
(2012)
|
||||
filename: T. Asao - Hydroponics -A Standard Methodology for Plant Biological Researches-Intech
|
||||
(2012).pdf
|
||||
description: 'Educational resource: T. Asao - Hydroponics -A Standard Methodology
|
||||
for Plant Biological Researches-Intech (2012)'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/T. Asao - Hydroponics -A Standard Methodology for Plant Biological
|
||||
Researches-Intech (2012).pdf
|
||||
- title: Oner, S. T - Cannabis Sativa. Volume 3, The essential guide to the world's
|
||||
finest marijuana strains (2014, Green Candy Press,Perseus Books, LLC)
|
||||
filename: Oner, S. T - Cannabis Sativa. Volume 3, The essential guide to the world's
|
||||
finest marijuana strains (2014, Green Candy Press,Perseus Books, LLC) - libgen.li.epub
|
||||
description: 'Educational resource: Oner, S. T - Cannabis Sativa. Volume 3, The
|
||||
essential guide to the world''s finest marijuana strains (2014, Green Candy Press,Perseus
|
||||
Books, LLC)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Oner, S. T - Cannabis Sativa. Volume 3, The essential guide to the
|
||||
world's finest marijuana strains (2014, Green Candy Press,Perseus Books, LLC)
|
||||
- libgen.li.epub
|
||||
- title: Growing Marijuana Mastery The Easiest and Complete Handbook to Growing Weed
|
||||
Indoor and Outdoor – Your Weed Growers Guide With Secrets for Big Bug Harvest
|
||||
filename: Growing Marijuana Mastery_ The Easiest and Complete Handbook to Growing
|
||||
Weed Indoor and Outdoor – Your Weed Growers Guide With Secrets for Big Bug Harvest.pdf
|
||||
description: 'Educational resource: Growing Marijuana Mastery The Easiest and Complete
|
||||
Handbook to Growing Weed Indoor and Outdoor – Your Weed Growers Guide With Secrets
|
||||
for Big Bug Harvest'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Growing Marijuana Mastery_ The Easiest and Complete Handbook to Growing
|
||||
Weed Indoor and Outdoor – Your Weed Growers Guide With Secrets for Big Bug Harvest.pdf
|
||||
- title: George Van Patten - Gardening Indoors with Soil & Hydroponics , Fifth Edition
|
||||
(2007, Van Patten Publishing)
|
||||
filename: George Van Patten - Gardening Indoors with Soil & Hydroponics , Fifth
|
||||
Edition (2007, Van Patten Publishing) - libgen.li.pdf
|
||||
description: 'Educational resource: George Van Patten - Gardening Indoors with Soil
|
||||
& Hydroponics , Fifth Edition (2007, Van Patten Publishing)'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/George Van Patten - Gardening Indoors with Soil & Hydroponics , Fifth
|
||||
Edition (2007, Van Patten Publishing) - libgen.li.pdf
|
||||
- title: Mahmoud A. ElSohly (editor) - Cannabis Chemistry and Biology Fundamentals
|
||||
(2023, De Gruyter) [10.1515 9783110718362]
|
||||
filename: Mahmoud A. ElSohly (editor) - Cannabis Chemistry and Biology_ Fundamentals
|
||||
(2023, De Gruyter) [10.1515_9783110718362] - libgen.li.pdf
|
||||
description: 'Educational resource: Mahmoud A. ElSohly (editor) - Cannabis Chemistry
|
||||
and Biology Fundamentals (2023, De Gruyter) [10.1515 9783110718362]'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Mahmoud A. ElSohly (editor) - Cannabis Chemistry and Biology_ Fundamentals
|
||||
(2023, De Gruyter) [10.1515_9783110718362] - libgen.li.pdf
|
||||
- title: Basil John Mason, Lyn Morgan, Peta Jinnath Abdul, Marie Beerman - Nutrients
|
||||
for Hydroponics Tissue Culture (2018)
|
||||
filename: Basil John Mason, Lyn Morgan, Peta Jinnath Abdul, Marie Beerman - Nutrients
|
||||
for Hydroponics Tissue Culture (2018) - libgen.li.pdf
|
||||
description: 'Educational resource: Basil John Mason, Lyn Morgan, Peta Jinnath Abdul,
|
||||
Marie Beerman - Nutrients for Hydroponics Tissue Culture (2018)'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Basil John Mason, Lyn Morgan, Peta Jinnath Abdul, Marie Beerman -
|
||||
Nutrients for Hydroponics Tissue Culture (2018) - libgen.li.pdf
|
||||
- title: Cervantes, Jorge - Marijuana Grow Basics The Easy Guide for Cannabis Aficionados
|
||||
(2009, Van Patten Publishing)
|
||||
filename: Cervantes, Jorge - Marijuana Grow Basics_ The Easy Guide for Cannabis
|
||||
Aficionados (2009, Van Patten Publishing) - libgen.li.pdf
|
||||
description: 'Educational resource: Cervantes, Jorge - Marijuana Grow Basics The
|
||||
Easy Guide for Cannabis Aficionados (2009, Van Patten Publishing)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Cervantes, Jorge - Marijuana Grow Basics_ The Easy Guide for Cannabis
|
||||
Aficionados (2009, Van Patten Publishing) - libgen.li.pdf
|
||||
- title: DIY Home Hydroponics For Beginners - The Essential Guide To Turn Your Backyard
|
||||
Into A Farm
|
||||
filename: DIY Home Hydroponics For Beginners - The Essential Guide To Turn Your
|
||||
Backyard Into A Farm.epub
|
||||
description: 'Educational resource: DIY Home Hydroponics For Beginners - The Essential
|
||||
Guide To Turn Your Backyard Into A Farm'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/DIY Home Hydroponics For Beginners - The Essential Guide To Turn
|
||||
Your Backyard Into A Farm.epub
|
||||
- title: 100 Best Cannabis Strains - A Pocket Guide for Medicinal and Recreational
|
||||
Use
|
||||
filename: 100 Best Cannabis Strains - A Pocket Guide for Medicinal and Recreational
|
||||
Use.epub
|
||||
description: 'Educational resource: 100 Best Cannabis Strains - A Pocket Guide for
|
||||
Medicinal and Recreational Use'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/100 Best Cannabis Strains - A Pocket Guide for Medicinal and Recreational
|
||||
Use.epub
|
||||
- title: '#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
|
||||
filename: '#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: #4 Marijuana Horticulture - The Indoor-Outdoor
|
||||
Medical Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#4
|
||||
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: '#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
|
||||
filename: '#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: #5 Marijuana Horticulture - The Indoor-Outdoor
|
||||
Medical Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#5
|
||||
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: '#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
|
||||
filename: '#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: #6 Marijuana Horticulture - The Indoor-Outdoor
|
||||
Medical Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#6
|
||||
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: '#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
|
||||
filename: '#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: #1 Marijuana Horticulture - The Indoor-Outdoor
|
||||
Medical Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#1
|
||||
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: '#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
|
||||
filename: '#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: #2 Marijuana Horticulture - The Indoor-Outdoor
|
||||
Medical Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#2
|
||||
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: '#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
|
||||
filename: '#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
|
||||
description: 'Educational resource: #3 Marijuana Horticulture - The Indoor-Outdoor
|
||||
Medical Growers Bible'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#3
|
||||
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
|
||||
- title: Jesse Ventura's Marijuana Manifesto
|
||||
filename: Jesse Ventura's Marijuana Manifesto.epub
|
||||
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Jesse Ventura's Marijuana Manifesto.epub
|
||||
- title: GROW GREAT WEED Personal & Med - Dale Denton
|
||||
filename: GROW GREAT WEED_ Personal & Med - Dale Denton.pdf
|
||||
description: 'Educational resource: GROW GREAT WEED Personal & Med - Dale Denton'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/GROW GREAT WEED_ Personal & Med - Dale Denton.pdf
|
||||
- title: Hydroponics Gardening Guide
|
||||
filename: Hydroponics Gardening Guide.epub
|
||||
description: 'Educational resource: Hydroponics Gardening Guide'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponics Gardening Guide.epub
|
||||
- title: How to Grow Marijuana - 3 Boo...
|
||||
filename: How to Grow Marijuana - 3 Boo....pdf
|
||||
description: 'Educational resource: How to Grow Marijuana - 3 Boo...'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/How to Grow Marijuana - 3 Boo....pdf
|
||||
- title: Ed Rosenthal Robert Flannery Angela Bacca Tommy Chong - Cannabis Grower's
|
||||
Handbook The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
|
||||
Archives)
|
||||
filename: Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis Grower's
|
||||
Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
|
||||
Archives) - libgen.li_ocr.pdf
|
||||
description: 'Educational resource: Ed Rosenthal Robert Flannery Angela Bacca Tommy
|
||||
Chong - Cannabis Grower''s Handbook The Complete Guide to Marijuana and Hemp
|
||||
Cultivation (2021, Quick American Archives)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis
|
||||
Grower's Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021,
|
||||
Quick American Archives) - libgen.li_ocr.pdf
|
||||
- title: '[Big of Buds] Ed Rosenthal - The Big Book of Buds More Marijuana Varieties
|
||||
from the World''s Greatest Seed Breeders (2007, Quick Trading Company Ed Rosenthal)'
|
||||
filename: '[Big of Buds] Ed Rosenthal - The Big Book of Buds_ More Marijuana Varieties
|
||||
from the World''s Greatest Seed Breeders (2007, Quick Trading Company_Ed Rosenthal)
|
||||
- libgen.li.epub'
|
||||
description: 'Educational resource: [Big of Buds] Ed Rosenthal - The Big Book of
|
||||
Buds More Marijuana Varieties from the World''s Greatest Seed Breeders (2007,
|
||||
Quick Trading Company Ed Rosenthal)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/[Big of Buds] Ed Rosenthal - The Big Book of Buds_ More Marijuana
|
||||
Varieties from the World's Greatest Seed Breeders (2007, Quick Trading Company_Ed
|
||||
Rosenthal) - libgen.li.epub
|
||||
- title: Storm - Marijuana Hydroponics High-Tech Water Culture (2016, Ronin Publishing)
|
||||
filename: Storm - Marijuana Hydroponics_ High-Tech Water Culture (2016, Ronin Publishing)
|
||||
- libgen.li.epub
|
||||
description: 'Educational resource: Storm - Marijuana Hydroponics High-Tech Water
|
||||
Culture (2016, Ronin Publishing)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Storm - Marijuana Hydroponics_ High-Tech Water Culture (2016, Ronin
|
||||
Publishing) - libgen.li.epub
|
||||
- title: Ed Rosenthal - Closet Cultivator Indoor Marijuana Cultivation Made Easy
|
||||
(1999, Quick American Archives)
|
||||
filename: Ed Rosenthal - Closet Cultivator_ Indoor Marijuana Cultivation Made Easy
|
||||
(1999, Quick American Archives) - libgen.li.pdf
|
||||
description: 'Educational resource: Ed Rosenthal - Closet Cultivator Indoor Marijuana
|
||||
Cultivation Made Easy (1999, Quick American Archives)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Ed Rosenthal - Closet Cultivator_ Indoor Marijuana Cultivation Made
|
||||
Easy (1999, Quick American Archives) - libgen.li.pdf
|
||||
- title: What a Plant Knows - A Field Guide to the Senses By Daniel Chamovitz
|
||||
filename: What a Plant Knows - A Field Guide to the Senses By Daniel Chamovitz.epub
|
||||
description: 'Educational resource: What a Plant Knows - A Field Guide to the Senses
|
||||
By Daniel Chamovitz'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/What a Plant Knows - A Field Guide to the Senses By Daniel Chamovitz.epub
|
||||
- title: Youbin Zheng - Handbook of Cannabis Production in Controlled Environments
|
||||
(2022, CRC Press)
|
||||
filename: Youbin Zheng - Handbook of Cannabis Production in Controlled Environments
|
||||
(2022, CRC Press) - libgen.li.pdf
|
||||
description: 'Educational resource: Youbin Zheng - Handbook of Cannabis Production
|
||||
in Controlled Environments (2022, CRC Press)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Youbin Zheng - Handbook of Cannabis Production in Controlled Environments
|
||||
(2022, CRC Press) - libgen.li.pdf
|
||||
- title: Gurevitch J. The Ecology of Plants 3ed 2020
|
||||
filename: Gurevitch J. The Ecology of Plants 3ed 2020.pdf
|
||||
description: 'Educational resource: Gurevitch J. The Ecology of Plants 3ed 2020'
|
||||
category: Science
|
||||
date: '2024-03-25'
|
||||
file: /library/Gurevitch J. The Ecology of Plants 3ed 2020.pdf
|
||||
- title: alfonso Garcia - Growing Marijuana Mastery The Advanced Guide to grow Cannabis Tips
|
||||
and Techniques for Advanced Cultivation of Medical Marijua (2020, Alfonso Garcia)
|
||||
filename: alfonso Garcia - Growing Marijuana Mastery_ The Advanced Guide to grow
|
||||
Cannabis_ Tips and Techniques for Advanced Cultivation of Medical Marijua (2020,
|
||||
Alfonso Garcia) - libgen.li.epub
|
||||
description: 'Educational resource: alfonso Garcia - Growing Marijuana Mastery The
|
||||
Advanced Guide to grow Cannabis Tips and Techniques for Advanced Cultivation
|
||||
of Medical Marijua (2020, Alfonso Garcia)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/alfonso Garcia - Growing Marijuana Mastery_ The Advanced Guide to
|
||||
grow Cannabis_ Tips and Techniques for Advanced Cultivation of Medical Marijua
|
||||
(2020, Alfonso Garcia) - libgen.li.epub
|
||||
- title: Robert Connell Clarke - Marijuana Botany - An Advanced Study The Propagation
|
||||
and Breeding of Distinctive Cannabis (0)
|
||||
filename: Robert Connell Clarke - Marijuana Botany - An Advanced Study_ The Propagation
|
||||
and Breeding of Distinctive Cannabis (0) - libgen.li.pdf
|
||||
description: 'Educational resource: Robert Connell Clarke - Marijuana Botany - An
|
||||
Advanced Study The Propagation and Breeding of Distinctive Cannabis (0)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Robert Connell Clarke - Marijuana Botany - An Advanced Study_ The
|
||||
Propagation and Breeding of Distinctive Cannabis (0) - libgen.li.pdf
|
||||
- title: Ed Rosenthal Robert Flannery Angela Bacca Tommy Chong - Cannabis Grower's
|
||||
Handbook The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
|
||||
Archives)
|
||||
filename: Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis Grower's
|
||||
Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
|
||||
Archives) - libgen.li.epub
|
||||
description: 'Educational resource: Ed Rosenthal Robert Flannery Angela Bacca Tommy
|
||||
Chong - Cannabis Grower''s Handbook The Complete Guide to Marijuana and Hemp
|
||||
Cultivation (2021, Quick American Archives)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis
|
||||
Grower's Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021,
|
||||
Quick American Archives) - libgen.li.epub
|
||||
- title: True Living Organics - The Ultimate Guide to Growing All-Natural Marijuana
|
||||
Indoors (2012)
|
||||
filename: True Living Organics - The Ultimate Guide to Growing All-Natural Marijuana
|
||||
Indoors (2012).epub
|
||||
description: 'Educational resource: True Living Organics - The Ultimate Guide to
|
||||
Growing All-Natural Marijuana Indoors (2012)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/True Living Organics - The Ultimate Guide to Growing All-Natural
|
||||
Marijuana Indoors (2012).epub
|
||||
- title: Michael Starks - Marijuana Chemistry-Ronin Publishing (1993)
|
||||
filename: Michael Starks - Marijuana Chemistry-Ronin Publishing (1993).pdf
|
||||
description: 'Educational resource: Michael Starks - Marijuana Chemistry-Ronin Publishing
|
||||
(1993)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Michael Starks - Marijuana Chemistry-Ronin Publishing (1993).pdf
|
||||
- title: Kleier C. Plant Science. An Introduction to Botany (Course Guidebook) 2017
|
||||
filename: Kleier C. Plant Science. An Introduction to Botany (Course Guidebook)
|
||||
2017.pdf
|
||||
description: 'Educational resource: Kleier C. Plant Science. An Introduction to
|
||||
Botany (Course Guidebook) 2017'
|
||||
category: Science
|
||||
date: '2024-03-25'
|
||||
file: /library/Kleier C. Plant Science. An Introduction to Botany (Course Guidebook)
|
||||
2017.pdf
|
||||
- title: Hydroponic Food Production - A Definitive Guidebook (7th Ed)(gnv64)
|
||||
filename: Hydroponic Food Production - A Definitive Guidebook (7th Ed)(gnv64).pdf
|
||||
description: 'Educational resource: Hydroponic Food Production - A Definitive Guidebook
|
||||
(7th Ed)(gnv64)'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/Hydroponic Food Production - A Definitive Guidebook (7th Ed)(gnv64).pdf
|
||||
- title: Dan Michaels - Higher The Lore, Legends, and Legacy of Cannabis (2023, Ten
|
||||
Speed Press)
|
||||
filename: Dan Michaels - Higher_ The Lore, Legends, and Legacy of Cannabis (2023,
|
||||
Ten Speed Press) - libgen.li.epub
|
||||
description: 'Educational resource: Dan Michaels - Higher The Lore, Legends, and
|
||||
Legacy of Cannabis (2023, Ten Speed Press)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Dan Michaels - Higher_ The Lore, Legends, and Legacy of Cannabis
|
||||
(2023, Ten Speed Press) - libgen.li.epub
|
||||
- title: MM Horticulture complete
|
||||
filename: MM Horticulture complete.pdf
|
||||
description: 'Educational resource: MM Horticulture complete'
|
||||
category: General
|
||||
date: '2024-03-25'
|
||||
file: /library/MM Horticulture complete.pdf
|
||||
- title: John Mason - Commercial Hydroponics 3rd Edition
|
||||
filename: John Mason - Commercial Hydroponics 3rd Edition.pdf
|
||||
description: 'Educational resource: John Mason - Commercial Hydroponics 3rd Edition'
|
||||
category: Systems
|
||||
date: '2024-03-25'
|
||||
file: /library/John Mason - Commercial Hydroponics 3rd Edition.pdf
|
||||
- title: Ross Tillman - DIY Aquaponics Marijuana Growing at Home Learn to Grow Most
|
||||
Potent THC Cannabis Ever! (2019, Grelot Books Publishing Company)
|
||||
filename: Ross Tillman - DIY Aquaponics Marijuana Growing at Home Learn to Grow
|
||||
Most Potent THC Cannabis Ever! (2019, Grelot Books Publishing Company) - libgen.li.epub
|
||||
description: 'Educational resource: Ross Tillman - DIY Aquaponics Marijuana Growing
|
||||
at Home Learn to Grow Most Potent THC Cannabis Ever! (2019, Grelot Books Publishing
|
||||
Company)'
|
||||
category: Cultivation
|
||||
date: '2024-03-25'
|
||||
file: /library/Ross Tillman - DIY Aquaponics Marijuana Growing at Home Learn to
|
||||
Grow Most Potent THC Cannabis Ever! (2019, Grelot Books Publishing Company) -
|
||||
libgen.li.epub
|
||||
2916
package-lock.json
generated
Normal file
2916
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "hydro-sterile",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"hugo-bin": "^0.149.2",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"tailwindcss": "^4.1.18"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
flask>=2.3.0
|
||||
flask-cors>=4.0.0
|
||||
bcrypt>=4.0.0
|
||||
PyJWT>=2.8.0
|
||||
ollama>=0.1.0
|
||||
pdfplumber>=0.10.0
|
||||
pyyaml>=6.0
|
||||
|
||||
112
scripts/CHAT_API_README.md
Normal file
112
scripts/CHAT_API_README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Chat API Server
|
||||
|
||||
This Flask-based API server provides a secure interface for the chat feature, connecting users to the Ollama model.
|
||||
|
||||
## Features
|
||||
|
||||
- **Secure**: Input sanitization, rate limiting, CORS protection
|
||||
- **Rate Limited**: 10 requests per minute per IP address
|
||||
- **Context Aware**: Maintains conversation history (last 5 messages)
|
||||
- **Auto-restart**: Runs as systemd service with automatic restart
|
||||
|
||||
## Configuration
|
||||
|
||||
**Environment Variables:**
|
||||
- `OLLAMA_HOST`: Ollama endpoint (default: `http://10.30.20.110:11434`)
|
||||
- `OLLAMA_MODEL`: Model name (default: `drjones-posts-to-much`)
|
||||
- `PORT`: API server port (default: `5000`)
|
||||
- `CHAT_API_KEY`: Optional API key for additional security (not set by default)
|
||||
|
||||
## Service Management
|
||||
|
||||
```bash
|
||||
# Start service
|
||||
sudo systemctl start hugo-chat-api.service
|
||||
|
||||
# Stop service
|
||||
sudo systemctl stop hugo-chat-api.service
|
||||
|
||||
# Enable at boot
|
||||
sudo systemctl enable hugo-chat-api.service
|
||||
|
||||
# Check status
|
||||
sudo systemctl status hugo-chat-api.service
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u hugo-chat-api.service -f
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### POST `/api/chat`
|
||||
|
||||
Send a chat message and get AI response.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"message": "How do I maintain sterile conditions?",
|
||||
"history": [
|
||||
{"role": "user", "content": "Previous message"},
|
||||
{"role": "assistant", "content": "Previous response"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"response": "AI response text...",
|
||||
"model": "drjones-posts-to-much"
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/api/health`
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"model": "drjones-posts-to-much",
|
||||
"ollama_host": "http://10.30.20.110:11434"
|
||||
}
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Rate Limiting**: 10 requests per minute per IP address
|
||||
2. **Input Sanitization**: Removes dangerous characters, limits length to 1000 chars
|
||||
3. **CORS Protection**: Only allows requests from configured origins
|
||||
4. **Error Handling**: Graceful error handling with user-friendly messages
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip3 install flask flask-cors ollama
|
||||
```
|
||||
|
||||
2. Enable and start service:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable hugo-chat-api.service
|
||||
sudo systemctl start hugo-chat-api.service
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Service won't start:**
|
||||
- Check logs: `sudo journalctl -u hugo-chat-api.service -n 50`
|
||||
- Verify Ollama is accessible: `curl http://10.30.20.110:11434/api/tags`
|
||||
- Check port 5000 is available: `sudo lsof -i :5000`
|
||||
|
||||
**Rate limit errors:**
|
||||
- Normal behavior - users are limited to 10 requests per minute
|
||||
- Rate limit resets after 1 minute
|
||||
|
||||
**Connection errors:**
|
||||
- Verify Ollama endpoint is correct and accessible
|
||||
- Check firewall rules allow connections to port 5000
|
||||
|
||||
171
scripts/README.md
Normal file
171
scripts/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# HydroLord Website Automation Scripts
|
||||
|
||||
This directory contains automation scripts for the HydroLord Hugo website.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `generate_blog_post.py`
|
||||
|
||||
Generates a new blog post using Ollama's AI model and adds it to the Hugo blog.
|
||||
|
||||
**Configuration:**
|
||||
- **Ollama Endpoint:** `http://10.30.20.110:11434`
|
||||
- **Model:** `drjones-posts-to-much`
|
||||
- **Output Directory:** `/root/hydro-sterile/content/blog/`
|
||||
- **Schedule:** Runs automatically every 3 days via systemd timer
|
||||
- **Randomization:** Each run uses a randomly varied prompt to ensure unique content
|
||||
|
||||
**Features:**
|
||||
- Automatically finds the next available post number
|
||||
- Generates title and summary from content
|
||||
- Creates proper Hugo frontmatter
|
||||
- Fails quietly if Ollama endpoint is unavailable (will retry in 3 days)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 generate_blog_post.py
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
If the Ollama endpoint is down, the script exits gracefully with code 0 (no error) to prevent systemd from logging failures. It will automatically retry in 3 days when the timer runs again.
|
||||
|
||||
### `start_hugo_server.sh`
|
||||
|
||||
Starts the Hugo development server for the website.
|
||||
|
||||
**Configuration:**
|
||||
- **Port:** 1313 (configurable via `HUGO_PORT` env var)
|
||||
- **Bind Address:** 0.0.0.0 (all interfaces, configurable via `HUGO_BIND` env var)
|
||||
- **Base URL:** Read from `hugo.yaml`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./start_hugo_server.sh
|
||||
```
|
||||
|
||||
## Systemd Services
|
||||
|
||||
### Hugo Website Service
|
||||
|
||||
**Service:** `hugo-hydrolord.service`
|
||||
|
||||
Starts the Hugo server at boot and keeps it running.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Enable service to start at boot
|
||||
sudo systemctl enable hugo-hydrolord.service
|
||||
|
||||
# Start service now
|
||||
sudo systemctl start hugo-hydrolord.service
|
||||
|
||||
# Check status
|
||||
sudo systemctl status hugo-hydrolord.service
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u hugo-hydrolord.service -f
|
||||
```
|
||||
|
||||
### Blog Post Generator
|
||||
|
||||
**Service:** `hugo-blog-generator.service`
|
||||
**Timer:** `hugo-blog-generator.timer`
|
||||
|
||||
Automatically generates blog posts every 3 days.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Enable timer
|
||||
sudo systemctl enable hugo-blog-generator.timer
|
||||
|
||||
# Start timer
|
||||
sudo systemctl start hugo-blog-generator.timer
|
||||
|
||||
# Check timer status
|
||||
sudo systemctl status hugo-blog-generator.timer
|
||||
|
||||
# List all timers
|
||||
systemctl list-timers hugo-blog-generator.timer
|
||||
|
||||
# Run generator manually (for testing)
|
||||
sudo systemctl start hugo-blog-generator.service
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u hugo-blog-generator.service -f
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Install Python dependencies:**
|
||||
```bash
|
||||
pip3 install ollama
|
||||
```
|
||||
|
||||
2. **Make scripts executable:**
|
||||
```bash
|
||||
chmod +x /root/hydro-sterile/scripts/*.py
|
||||
chmod +x /root/hydro-sterile/scripts/*.sh
|
||||
```
|
||||
|
||||
3. **Enable and start Hugo service:**
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable hugo-hydrolord.service
|
||||
sudo systemctl start hugo-hydrolord.service
|
||||
```
|
||||
|
||||
4. **Enable and start blog generator timer:**
|
||||
```bash
|
||||
sudo systemctl enable hugo-blog-generator.timer
|
||||
sudo systemctl start hugo-blog-generator.timer
|
||||
```
|
||||
|
||||
5. **Verify everything is running:**
|
||||
```bash
|
||||
sudo systemctl status hugo-hydrolord.service
|
||||
sudo systemctl status hugo-blog-generator.timer
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hugo Server Not Starting
|
||||
|
||||
- Check if port 1313 is already in use: `sudo lsof -i :1313`
|
||||
- Check logs: `sudo journalctl -u hugo-hydrolord.service -n 50`
|
||||
- Verify Hugo is installed: `which hugo` or check `node_modules/.bin/hugo`
|
||||
|
||||
### Blog Generator Not Running
|
||||
|
||||
- Check if Ollama endpoint is accessible: `curl http://10.30.20.110:11434/api/tags`
|
||||
- Check timer status: `systemctl list-timers hugo-blog-generator.timer`
|
||||
- View logs: `sudo journalctl -u hugo-blog-generator.service -n 50`
|
||||
- Test script manually: `python3 /root/hydro-sterile/scripts/generate_blog_post.py`
|
||||
|
||||
### Ollama Endpoint Down
|
||||
|
||||
The script is designed to fail quietly if Ollama is unavailable. It will exit with code 0 (success) and retry automatically in 3 days. No action needed.
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
To change the Ollama endpoint or model, edit `/root/hydro-sterile/scripts/generate_blog_post.py` and modify the constants at the top:
|
||||
|
||||
```python
|
||||
OLLAMA_HOST = "http://10.30.20.110:11434"
|
||||
MODEL = "drjones-posts-to-much"
|
||||
```
|
||||
|
||||
**Note:** The script now includes random prompt variations to ensure each blog post is unique. The model generates titles with credentials, which the script automatically extracts and separates from the article content.
|
||||
|
||||
After making changes, restart the timer:
|
||||
```bash
|
||||
sudo systemctl restart hugo-blog-generator.timer
|
||||
```
|
||||
|
||||
## Files Location
|
||||
|
||||
- **Scripts:** `/root/hydro-sterile/scripts/`
|
||||
- **Systemd Services:** `/etc/systemd/system/hugo-*.service`
|
||||
- **Systemd Timers:** `/etc/systemd/system/hugo-*.timer`
|
||||
- **Blog Posts:** `/root/hydro-sterile/content/blog/`
|
||||
- **Logs:** `journalctl -u hugo-hydrolord.service` or `journalctl -u hugo-blog-generator.service`
|
||||
|
||||
BIN
scripts/__pycache__/chat_api.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/chat_api.cpython-310.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/generate_blog_post.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/generate_blog_post.cpython-310.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/membership_api.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/membership_api.cpython-310.pyc
Normal file
Binary file not shown.
348
scripts/chat_api.py
Executable file
348
scripts/chat_api.py
Executable file
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Chat API Server for Ollama Integration
|
||||
|
||||
This Flask server provides a secure API endpoint for the chat interface.
|
||||
It handles rate limiting, input validation, and communicates with Ollama.
|
||||
|
||||
Usage:
|
||||
python3 chat_api.py
|
||||
|
||||
Configuration:
|
||||
- Ollama endpoint: 10.30.20.110:11434
|
||||
- Model: drjones-posts-to-much
|
||||
- Port: 5000 (configurable via PORT env var)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
from ollama import Client
|
||||
except ImportError:
|
||||
print("ERROR: ollama Python library not installed. Run: pip3 install ollama", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import pdfplumber
|
||||
PDF_SUPPORT = True
|
||||
except ImportError:
|
||||
PDF_SUPPORT = False
|
||||
print("WARNING: pdfplumber not installed. PDF text extraction disabled.", file=sys.stderr)
|
||||
|
||||
# Configuration
|
||||
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://10.30.20.110:11434")
|
||||
MODEL = os.getenv("OLLAMA_MODEL", "drjones-posts-to-much")
|
||||
PORT = int(os.getenv("PORT", 5000))
|
||||
API_KEY = os.getenv("CHAT_API_KEY", "") # Optional API key for additional security
|
||||
LIBRARY_DIR = Path("/root/hydro-sterile/static/library")
|
||||
LIBRARY_YAML = Path("/root/hydro-sterile/data/library.yaml")
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB max file size
|
||||
MAX_TEXT_LENGTH = 5000 # Max characters to extract from PDF
|
||||
|
||||
# Rate limiting storage (in-memory, simple implementation)
|
||||
rate_limits = {}
|
||||
|
||||
# System prompt for the chat
|
||||
SYSTEM_PROMPT = """You are Dr. Jones, a sterile hydroponics expert with 20 years of experience growing cannabis in a legal state.
|
||||
You provide practical, direct, and knowledgeable advice about sterile hydroponics, cannabis cultivation, and growing techniques.
|
||||
Keep responses concise (2-4 paragraphs), practical, and actionable. Write in first person (I, me, my) and be conversational."""
|
||||
|
||||
app = Flask(__name__)
|
||||
# Allow CORS from the website domain and local development
|
||||
CORS(app, origins=[
|
||||
"https://hydrolord.thetempleofdoom.com",
|
||||
"http://hydrolord.thetempleofdoom.com",
|
||||
"http://localhost:1313",
|
||||
"http://127.0.0.1:1313",
|
||||
"http://10.30.20.243",
|
||||
"http://10.30.20.243:1313"
|
||||
])
|
||||
|
||||
def get_client_ip():
|
||||
"""Get client IP address for rate limiting."""
|
||||
if request.headers.get('X-Forwarded-For'):
|
||||
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
||||
return request.remote_addr
|
||||
|
||||
def rate_limit(max_per_minute=10, window_minutes=1):
|
||||
"""Simple rate limiting decorator."""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
client_ip = get_client_ip()
|
||||
now = time.time()
|
||||
window_seconds = window_minutes * 60
|
||||
|
||||
# Clean old entries
|
||||
rate_limits[client_ip] = [
|
||||
timestamp for timestamp in rate_limits.get(client_ip, [])
|
||||
if now - timestamp < window_seconds
|
||||
]
|
||||
|
||||
# Check rate limit
|
||||
if len(rate_limits.get(client_ip, [])) >= max_per_minute:
|
||||
return jsonify({
|
||||
"error": "Rate limit exceeded. Please wait a moment before sending another message."
|
||||
}), 429
|
||||
|
||||
# Add current request
|
||||
if client_ip not in rate_limits:
|
||||
rate_limits[client_ip] = []
|
||||
rate_limits[client_ip].append(now)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def sanitize_input(text):
|
||||
"""Sanitize user input to prevent injection attacks."""
|
||||
if not text or not isinstance(text, str):
|
||||
return ""
|
||||
|
||||
# Remove potentially dangerous characters
|
||||
text = re.sub(r'[<>]', '', text)
|
||||
|
||||
# Limit length
|
||||
text = text[:1000].strip()
|
||||
|
||||
return text
|
||||
|
||||
def load_library_files():
|
||||
"""Load library file list from YAML."""
|
||||
try:
|
||||
with open(LIBRARY_YAML, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
files = []
|
||||
for entry in data or []:
|
||||
if isinstance(entry, dict) and 'title' in entry and 'file' in entry:
|
||||
file_path = entry.get('file', '')
|
||||
# Convert /library/ path to actual file path
|
||||
if file_path.startswith('/library/'):
|
||||
filename = file_path.replace('/library/', '')
|
||||
full_path = LIBRARY_DIR / filename
|
||||
if full_path.exists():
|
||||
files.append({
|
||||
'title': entry.get('title', ''),
|
||||
'filename': entry.get('filename', ''),
|
||||
'file': file_path,
|
||||
'category': entry.get('category', ''),
|
||||
'size': full_path.stat().st_size if full_path.exists() else 0
|
||||
})
|
||||
return files
|
||||
except Exception as e:
|
||||
print(f"Error loading library: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
def extract_pdf_text(file_path, max_length=MAX_TEXT_LENGTH):
|
||||
"""Extract text from PDF file."""
|
||||
if not PDF_SUPPORT:
|
||||
return None
|
||||
|
||||
try:
|
||||
full_path = LIBRARY_DIR / file_path.replace('/library/', '')
|
||||
|
||||
if not full_path.exists():
|
||||
return None
|
||||
|
||||
if full_path.stat().st_size > MAX_FILE_SIZE:
|
||||
return f"[File too large: {full_path.stat().st_size / 1024 / 1024:.1f}MB. Max size: {MAX_FILE_SIZE / 1024 / 1024}MB]"
|
||||
|
||||
text_parts = []
|
||||
with pdfplumber.open(str(full_path)) as pdf:
|
||||
for page in pdf.pages[:10]: # Limit to first 10 pages
|
||||
page_text = page.extract_text()
|
||||
if page_text:
|
||||
text_parts.append(page_text)
|
||||
if len(' '.join(text_parts)) > max_length:
|
||||
break
|
||||
|
||||
full_text = ' '.join(text_parts)
|
||||
if len(full_text) > max_length:
|
||||
full_text = full_text[:max_length] + "... [truncated]"
|
||||
|
||||
return full_text.strip()
|
||||
except Exception as e:
|
||||
print(f"Error extracting PDF text: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
@rate_limit(max_per_minute=10, window_minutes=1)
|
||||
def chat():
|
||||
"""Handle chat requests."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'message' not in data:
|
||||
return jsonify({"error": "Message is required"}), 400
|
||||
|
||||
message = sanitize_input(data['message'])
|
||||
|
||||
if not message:
|
||||
return jsonify({"error": "Message cannot be empty"}), 400
|
||||
|
||||
# Optional API key check
|
||||
if API_KEY and data.get('api_key') != API_KEY:
|
||||
return jsonify({"error": "Invalid API key"}), 401
|
||||
|
||||
# Get selected library files
|
||||
selected_files = data.get('files', [])
|
||||
file_context = ""
|
||||
|
||||
if selected_files:
|
||||
file_context_parts = []
|
||||
for file_ref in selected_files[:3]: # Limit to 3 files max
|
||||
if isinstance(file_ref, str):
|
||||
file_path = file_ref
|
||||
elif isinstance(file_ref, dict) and 'file' in file_ref:
|
||||
file_path = file_ref['file']
|
||||
else:
|
||||
continue
|
||||
|
||||
# Extract text from PDF
|
||||
pdf_text = extract_pdf_text(file_path)
|
||||
if pdf_text:
|
||||
# Get file title
|
||||
file_title = file_ref.get('title', '') if isinstance(file_ref, dict) else file_path
|
||||
file_context_parts.append(f"\n\n--- Content from: {file_title} ---\n{pdf_text}\n--- End of {file_title} ---")
|
||||
|
||||
if file_context_parts:
|
||||
file_context = "\n\n[The user has referenced the following library documents. Use this information to provide accurate, specific answers based on these sources:]\n" + "\n".join(file_context_parts)
|
||||
|
||||
# Get conversation history (last 5 messages for context)
|
||||
history = data.get('history', [])
|
||||
history_messages = []
|
||||
|
||||
# Build conversation history
|
||||
for msg in history[-5:]: # Last 5 messages
|
||||
if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
|
||||
history_messages.append({
|
||||
"role": msg['role'],
|
||||
"content": sanitize_input(msg['content'])
|
||||
})
|
||||
|
||||
# Add current user message with file context
|
||||
user_message = message
|
||||
if file_context:
|
||||
user_message = message + file_context
|
||||
|
||||
history_messages.append({
|
||||
"role": "user",
|
||||
"content": user_message
|
||||
})
|
||||
|
||||
# Call Ollama
|
||||
try:
|
||||
client = Client(host=OLLAMA_HOST)
|
||||
|
||||
# Build messages for Ollama
|
||||
ollama_messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT}
|
||||
]
|
||||
|
||||
# Add conversation history
|
||||
for msg in history_messages:
|
||||
ollama_messages.append({
|
||||
"role": msg['role'],
|
||||
"content": msg['content']
|
||||
})
|
||||
|
||||
# Generate response
|
||||
response = client.chat(
|
||||
model=MODEL,
|
||||
messages=ollama_messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
}
|
||||
)
|
||||
|
||||
assistant_response = response.message.content.strip()
|
||||
|
||||
return jsonify({
|
||||
"response": assistant_response,
|
||||
"model": MODEL
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ollama error: {e}", file=sys.stderr)
|
||||
return jsonify({
|
||||
"error": "Unable to connect to AI service. Please try again later."
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
print(f"API error: {e}", file=sys.stderr)
|
||||
return jsonify({
|
||||
"error": "An error occurred processing your request."
|
||||
}), 500
|
||||
|
||||
@app.route('/api/library/files', methods=['GET'])
|
||||
def list_library_files():
|
||||
"""List available library files."""
|
||||
try:
|
||||
files = load_library_files()
|
||||
# Filter to only PDF files for now
|
||||
pdf_files = [f for f in files if f['filename'].lower().endswith('.pdf')]
|
||||
return jsonify({
|
||||
"files": pdf_files,
|
||||
"count": len(pdf_files)
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error listing files: {e}", file=sys.stderr)
|
||||
return jsonify({
|
||||
"error": "Failed to load library files"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/library/preview', methods=['POST'])
|
||||
def preview_file():
|
||||
"""Preview/extract text from a library file."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
file_path = data.get('file')
|
||||
|
||||
if not file_path:
|
||||
return jsonify({"error": "File path required"}), 400
|
||||
|
||||
text = extract_pdf_text(file_path)
|
||||
|
||||
if text is None:
|
||||
return jsonify({
|
||||
"error": "Could not extract text from file. File may not be a PDF or may be corrupted."
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
"text": text,
|
||||
"length": len(text)
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error previewing file: {e}", file=sys.stderr)
|
||||
return jsonify({
|
||||
"error": "Failed to preview file"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({
|
||||
"status": "healthy",
|
||||
"model": MODEL,
|
||||
"ollama_host": OLLAMA_HOST,
|
||||
"pdf_support": PDF_SUPPORT
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"Starting Chat API server on port {PORT}")
|
||||
print(f"Ollama endpoint: {OLLAMA_HOST}")
|
||||
print(f"Model: {MODEL}")
|
||||
app.run(host='0.0.0.0', port=PORT, debug=False)
|
||||
|
||||
320
scripts/generate_blog_post.py
Executable file
320
scripts/generate_blog_post.py
Executable file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ollama Blog Post Generator
|
||||
|
||||
This script generates a new blog post using Ollama's drjones-posts-to-much model
|
||||
and adds it to the Hugo blog. It runs automatically every 3 days via systemd timer.
|
||||
|
||||
The script uses random prompt variations to ensure each blog post is unique.
|
||||
The model generates titles with credentials, which are automatically extracted
|
||||
and separated from the article content.
|
||||
|
||||
Usage:
|
||||
python3 generate_blog_post.py
|
||||
|
||||
Configuration:
|
||||
- Ollama endpoint: 10.30.20.110:11434
|
||||
- Model: drjones-posts-to-much
|
||||
- Blog directory: /root/hydro-sterile/content/blog/
|
||||
|
||||
Error Handling:
|
||||
If the Ollama endpoint is down, the script fails quietly (exits with code 0)
|
||||
to prevent systemd from logging errors. It will retry in 3 days.
|
||||
|
||||
Author: Dr. Jones
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from ollama import Client
|
||||
except ImportError:
|
||||
print("ERROR: ollama Python library not installed. Run: pip3 install ollama", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Configuration
|
||||
OLLAMA_HOST = "http://10.30.20.110:11434"
|
||||
MODEL = "drjones-posts-to-much"
|
||||
BLOG_DIR = Path("/root/hydro-sterile/content/blog")
|
||||
SITE_DIR = Path("/root/hydro-sterile")
|
||||
|
||||
# System prompt for generating blog posts
|
||||
SYSTEM_PROMPT = """You are Dr. Jones, a sterile hydroponics expert with 20 years of experience growing cannabis in a legal state.
|
||||
Your writing style is authentic, direct, and knowledgeable. You focus on sterile hydroponics methodology,
|
||||
growing techniques, and practical advice. Write blog posts that are:
|
||||
- 300-500 words
|
||||
- Focused on sterile hydroponics, cannabis cultivation, or related topics
|
||||
- Written in first person (I, me, my)
|
||||
- Practical and actionable
|
||||
- Authentic and conversational
|
||||
|
||||
The model will generate a title with credentials, then the article content."""
|
||||
|
||||
|
||||
def get_next_post_number():
|
||||
"""Find the next available post number by checking existing posts."""
|
||||
existing_posts = list(BLOG_DIR.glob("post-*.md"))
|
||||
if not existing_posts:
|
||||
return 1
|
||||
|
||||
numbers = []
|
||||
for post in existing_posts:
|
||||
try:
|
||||
# Extract number from filename like "post-1.md"
|
||||
num = int(post.stem.split("-")[1])
|
||||
numbers.append(num)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return max(numbers) + 1 if numbers else 1
|
||||
|
||||
|
||||
def generate_filename(post_number):
|
||||
"""Generate the filename for a new post."""
|
||||
return BLOG_DIR / f"post-{post_number}.md"
|
||||
|
||||
|
||||
def create_frontmatter(title, summary, image_path=None):
|
||||
"""Create YAML frontmatter for Hugo blog post."""
|
||||
date_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
frontmatter = f"""---
|
||||
title: "{title}"
|
||||
date: {date_str}
|
||||
image: "{image_path or '/images/IMG_1767.jpeg'}"
|
||||
summary: "{summary}"
|
||||
---
|
||||
"""
|
||||
return frontmatter
|
||||
|
||||
|
||||
def generate_random_prompt():
|
||||
"""Generate a random prompt variation to ensure different content each time."""
|
||||
# Different ways to ask for hydroponics knowledge
|
||||
prompt_variations = [
|
||||
"Share your wisdom about sterile hydroponics. What insights can you offer growers today?",
|
||||
"I'm seeking your expertise on hydroponic cultivation. What knowledge can you impart?",
|
||||
"Please share your knowledge about sterile hydroponics systems. What should growers know?",
|
||||
"I need your guidance on hydroponic growing. What can you teach me today?",
|
||||
"What hydroponic wisdom can you share? I'm eager to learn from your experience.",
|
||||
"Please enlighten me about sterile hydroponics. What key insights do you have?",
|
||||
"I'm asking for your hydroponic expertise. What valuable knowledge can you provide?",
|
||||
"Share your hydroponic knowledge with me. What should I know about sterile systems?",
|
||||
"I'm seeking your hydroponic wisdom. What can you teach growers today?",
|
||||
"Please provide your insights on sterile hydroponics. What's important to know?",
|
||||
"What can you tell me about hydroponic cultivation? I'm looking for your expertise.",
|
||||
"I need your hydroponic knowledge. What guidance can you offer?",
|
||||
"Share your experience with sterile hydroponics. What should growers understand?",
|
||||
"I'm asking for your hydroponic insights. What knowledge can you share?",
|
||||
"Please enlighten me about hydroponic systems. What wisdom do you have?",
|
||||
]
|
||||
|
||||
# Additional random elements to add variety
|
||||
topic_hints = [
|
||||
"",
|
||||
" Focus on practical techniques.",
|
||||
" Emphasize sterile methodology.",
|
||||
" Include real-world examples.",
|
||||
" Cover common mistakes to avoid.",
|
||||
" Discuss nutrient management.",
|
||||
" Talk about root health.",
|
||||
" Share system design tips.",
|
||||
" Address problem-solving.",
|
||||
" Include cultivation tips.",
|
||||
]
|
||||
|
||||
# Random opening phrases
|
||||
openings = [
|
||||
"",
|
||||
"Today, ",
|
||||
"In this post, ",
|
||||
"Let me share ",
|
||||
"Here's what I know: ",
|
||||
]
|
||||
|
||||
base_prompt = random.choice(prompt_variations)
|
||||
topic_hint = random.choice(topic_hints)
|
||||
opening = random.choice(openings)
|
||||
|
||||
# Combine with some randomness
|
||||
full_prompt = opening + base_prompt + topic_hint
|
||||
|
||||
return full_prompt
|
||||
|
||||
|
||||
def call_ollama():
|
||||
"""Call Ollama API to generate blog post content."""
|
||||
client = Client(host=OLLAMA_HOST)
|
||||
|
||||
try:
|
||||
# Generate random prompt for variety
|
||||
user_prompt = generate_random_prompt()
|
||||
|
||||
# Generate the blog post content
|
||||
response = client.chat(
|
||||
model=MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
options={
|
||||
"temperature": 0.8 + random.uniform(0, 0.2), # Random temperature between 0.8-1.0 for more variety
|
||||
"top_p": 0.9,
|
||||
"repeat_penalty": 1.5, # High repeat penalty to avoid repetitive content
|
||||
}
|
||||
)
|
||||
|
||||
content = response.message.content
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
# Fail quietly - don't raise exception, just return None
|
||||
# This allows the script to exit gracefully when Ollama is down
|
||||
print(f"Warning: Could not connect to Ollama at {OLLAMA_HOST}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def extract_title_and_summary(content):
|
||||
"""Extract title and summary from generated content.
|
||||
|
||||
The model generates title with credentials, then the article.
|
||||
We need to separate these and extract just the title and article content.
|
||||
"""
|
||||
lines = content.strip().split('\n')
|
||||
|
||||
# The model typically generates: "Title - Dr. Jones, Credentials\n\nArticle content"
|
||||
# Or: "Title\nDr. Jones, Credentials\n\nArticle content"
|
||||
# Or: "# Title\n\nDr. Jones, Credentials\n\nArticle content"
|
||||
|
||||
title = ""
|
||||
article_start_idx = 0
|
||||
credentials_found = False
|
||||
|
||||
# Look for title (usually first line or first few lines)
|
||||
# Title might be followed by credentials line, then blank line, then article
|
||||
for i, line in enumerate(lines):
|
||||
line_stripped = line.strip()
|
||||
|
||||
# Skip empty lines at the start
|
||||
if not line_stripped:
|
||||
continue
|
||||
|
||||
# If we haven't found title yet, this might be it
|
||||
if not title:
|
||||
# Remove markdown headers
|
||||
potential_title = line_stripped.replace('#', '').strip()
|
||||
|
||||
# Check if this line contains credentials (common patterns)
|
||||
if any(cred in potential_title.lower() for cred in ['dr.', 'dr ', 'phd', 'ph.d', 'credentials', 'expert', 'years']):
|
||||
# This might be credentials, skip it and look for title before it
|
||||
if i > 0:
|
||||
# Previous line might be title
|
||||
prev_line = lines[i-1].strip().replace('#', '').strip()
|
||||
if prev_line:
|
||||
title = prev_line
|
||||
article_start_idx = i + 1
|
||||
credentials_found = True
|
||||
break
|
||||
continue
|
||||
|
||||
# If line ends with common credential patterns, split it
|
||||
if ' - ' in potential_title or ' | ' in potential_title:
|
||||
parts = potential_title.split(' - ') if ' - ' in potential_title else potential_title.split(' | ')
|
||||
title = parts[0].strip()
|
||||
article_start_idx = i + 1
|
||||
credentials_found = True
|
||||
break
|
||||
else:
|
||||
# This might be the title, but check next line for credentials
|
||||
title = potential_title
|
||||
article_start_idx = i + 1
|
||||
|
||||
# Check if this is a credentials line (after title)
|
||||
elif not credentials_found and any(cred in line_stripped.lower() for cred in ['dr.', 'dr ', 'phd', 'ph.d', 'expert', 'years of experience']):
|
||||
credentials_found = True
|
||||
article_start_idx = i + 1
|
||||
break
|
||||
|
||||
# If we've passed potential title/credentials, this is article content
|
||||
elif title and not credentials_found and line_stripped:
|
||||
# No credentials found, article starts here
|
||||
article_start_idx = i
|
||||
break
|
||||
|
||||
# Clean up title
|
||||
if not title:
|
||||
# Fallback: use first non-empty line
|
||||
for line in lines:
|
||||
line_stripped = line.strip().replace('#', '').strip()
|
||||
if line_stripped:
|
||||
title = line_stripped
|
||||
break
|
||||
|
||||
if len(title) > 100:
|
||||
title = title[:97] + "..."
|
||||
|
||||
# Extract article content (skip title and credentials)
|
||||
article_lines = lines[article_start_idx:]
|
||||
article_content = '\n'.join(article_lines).strip()
|
||||
|
||||
# Generate summary from first paragraph of article
|
||||
summary = ""
|
||||
for line in article_lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and len(line) > 20:
|
||||
summary = line[:150]
|
||||
if len(line) > 150:
|
||||
summary += "..."
|
||||
break
|
||||
|
||||
if not summary:
|
||||
summary = "A new insight on sterile hydroponics and cultivation techniques."
|
||||
|
||||
return title, summary, article_content
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to generate and save blog post."""
|
||||
# Ensure blog directory exists
|
||||
BLOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Check if Ollama is available
|
||||
content = call_ollama()
|
||||
if content is None:
|
||||
# Fail quietly - exit with code 0 so systemd doesn't log errors
|
||||
print("Ollama endpoint unavailable. Will retry in 3 days.", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
# Get next post number
|
||||
post_number = get_next_post_number()
|
||||
filename = generate_filename(post_number)
|
||||
|
||||
# Extract title, summary, and article content (separate from credentials)
|
||||
title, summary, article_content = extract_title_and_summary(content)
|
||||
|
||||
# Create frontmatter
|
||||
frontmatter = create_frontmatter(title, summary)
|
||||
|
||||
# Combine frontmatter and article content (not the full content with credentials)
|
||||
full_content = frontmatter + "\n" + article_content + "\n"
|
||||
|
||||
# Write to file
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(full_content)
|
||||
print(f"Successfully created blog post: {filename}")
|
||||
print(f"Title: {title}")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to write blog post: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
478
scripts/membership_api.py
Normal file
478
scripts/membership_api.py
Normal file
@@ -0,0 +1,478 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Membership API Server
|
||||
|
||||
Handles user authentication, membership management, download tracking,
|
||||
testimonials, and secure contact form submissions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import secrets
|
||||
import re
|
||||
import smtplib
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
from flask_cors import CORS
|
||||
import bcrypt
|
||||
import jwt
|
||||
|
||||
# Configuration
|
||||
PORT = int(os.getenv("MEMBERSHIP_PORT", 80))
|
||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
|
||||
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "indiana.holmes8@gmail.com")
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", secrets.token_urlsafe(32))
|
||||
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
||||
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
|
||||
SMTP_USER = os.getenv("SMTP_USER", "")
|
||||
SMTP_PASS = os.getenv("SMTP_PASS", "")
|
||||
|
||||
# Static folder for Hugo build
|
||||
STATIC_FOLDER = "/root/hydro-sterile/public"
|
||||
|
||||
# Database path
|
||||
DB_PATH = Path("/root/hydro-sterile/db/members.db")
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
# Static folder for Hugo build
|
||||
STATIC_FOLDER = "/root/hydro-sterile/public"
|
||||
|
||||
# Rate limiting storage
|
||||
rate_limits = {}
|
||||
|
||||
def get_client_ip():
|
||||
"""Get client IP address."""
|
||||
if request.headers.get('X-Forwarded-For'):
|
||||
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
||||
return request.remote_addr or 'unknown'
|
||||
|
||||
def init_db():
|
||||
"""Initialize database with required tables."""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
|
||||
# Users table
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS users
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
approved_at TIMESTAMP,
|
||||
approved_by TEXT)''')
|
||||
|
||||
# Downloads table
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS downloads
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT NOT NULL,
|
||||
user_id INTEGER,
|
||||
file_path TEXT NOT NULL,
|
||||
downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id))''')
|
||||
|
||||
# Testimonials table
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS testimonials
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_name TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
approved BOOLEAN DEFAULT 0,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id))''')
|
||||
|
||||
# Sessions table
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS sessions
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
session_token TEXT UNIQUE NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id))''')
|
||||
|
||||
# Admin sessions table
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS admin_sessions
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_token TEXT UNIQUE NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL)''')
|
||||
|
||||
# Contact submissions table
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS contact_submissions
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id))''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def rate_limit(max_per_minute=10, window_minutes=1):
|
||||
"""Rate limiting decorator."""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
client_ip = get_client_ip()
|
||||
now = time.time()
|
||||
window_seconds = window_minutes * 60
|
||||
|
||||
if client_ip not in rate_limits:
|
||||
rate_limits[client_ip] = []
|
||||
|
||||
rate_limits[client_ip] = [
|
||||
timestamp for timestamp in rate_limits[client_ip]
|
||||
if now - timestamp < window_seconds
|
||||
]
|
||||
|
||||
if len(rate_limits[client_ip]) >= max_per_minute:
|
||||
return jsonify({"error": "Rate limit exceeded"}), 429
|
||||
|
||||
rate_limits[client_ip].append(now)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def sanitize_input(text, max_length=1000):
|
||||
"""Sanitize user input."""
|
||||
if not text or not isinstance(text, str):
|
||||
return ""
|
||||
text = re.sub(r'[<>]', '', text)
|
||||
return text[:max_length].strip()
|
||||
|
||||
def validate_email(email):
|
||||
"""Validate email format."""
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
return re.match(pattern, email) is not None
|
||||
|
||||
def send_email(to_email, subject, body_html, body_text=None):
|
||||
"""Send email via SMTP."""
|
||||
if not SMTP_HOST or not SMTP_USER or not SMTP_PASS:
|
||||
print(f"EMAIL NOT CONFIGURED: Would send to {to_email}: {subject}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = SMTP_USER
|
||||
msg['To'] = to_email
|
||||
|
||||
if body_text:
|
||||
msg.attach(MIMEText(body_text, 'plain'))
|
||||
msg.attach(MIMEText(body_html, 'html'))
|
||||
|
||||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
|
||||
server.starttls()
|
||||
server.login(SMTP_USER, SMTP_PASS)
|
||||
server.send_message(msg)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Email send error: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def require_auth(f):
|
||||
"""Require user authentication."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
if not token:
|
||||
return jsonify({"error": "Authentication required"}), 401
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
||||
user_id = payload.get('user_id')
|
||||
|
||||
# Verify session exists
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT user_id FROM sessions WHERE session_token = ? AND expires_at > ?',
|
||||
(token, datetime.utcnow()))
|
||||
session = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not session:
|
||||
return jsonify({"error": "Invalid session"}), 401
|
||||
|
||||
request.user_id = user_id
|
||||
return f(*args, **kwargs)
|
||||
except jwt.ExpiredSignatureError:
|
||||
return jsonify({"error": "Session expired"}), 401
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Invalid token"}), 401
|
||||
return decorated_function
|
||||
|
||||
def require_admin(f):
|
||||
"""Require admin authentication."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
if not token:
|
||||
return jsonify({"error": "Admin authentication required"}), 401
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT id FROM admin_sessions WHERE session_token = ? AND expires_at > ?',
|
||||
(token, datetime.utcnow()))
|
||||
session = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not session:
|
||||
return jsonify({"error": "Invalid admin session"}), 401
|
||||
|
||||
return f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Invalid admin token"}), 401
|
||||
return decorated_function
|
||||
|
||||
# Initialize database on startup
|
||||
init_db()
|
||||
|
||||
# ============================================================================
|
||||
# API Endpoints
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/auth/register', methods=['POST'])
|
||||
@rate_limit(max_per_minute=5, window_minutes=1)
|
||||
def register():
|
||||
"""Register a new user."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = sanitize_input(data.get('email', ''), max_length=255)
|
||||
password = data.get('password', '')
|
||||
name = sanitize_input(data.get('name', ''), max_length=255)
|
||||
|
||||
if not email or not password or not name:
|
||||
return jsonify({"error": "Email, password, and name are required"}), 400
|
||||
|
||||
if not validate_email(email):
|
||||
return jsonify({"error": "Invalid email format"}), 400
|
||||
|
||||
if len(password) < 8:
|
||||
return jsonify({"error": "Password must be at least 8 characters"}), 400
|
||||
|
||||
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute('INSERT INTO users (email, password_hash, name, status) VALUES (?, ?, ?, ?)',
|
||||
(email, password_hash, name, 'pending'))
|
||||
user_id = c.lastrowid
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
conn.close()
|
||||
return jsonify({"error": "Email already registered"}), 400
|
||||
conn.close()
|
||||
|
||||
email_body = f"<h2>New Membership Signup</h2><p>Name: {name}</p><p>Email: {email}</p>"
|
||||
send_email(ADMIN_EMAIL, f"New Membership Signup: {name}", email_body)
|
||||
|
||||
return jsonify({"message": "Registration successful. Pending approval.", "user_id": user_id}), 201
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Registration failed"}), 500
|
||||
|
||||
@app.route('/api/auth/login', methods=['POST'])
|
||||
@rate_limit(max_per_minute=5, window_minutes=1)
|
||||
def login():
|
||||
"""User login."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = sanitize_input(data.get('email', ''), max_length=255)
|
||||
password = data.get('password', '')
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT id, password_hash, status FROM users WHERE email = ?', (email,))
|
||||
user = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not user or not bcrypt.checkpw(password.encode('utf-8'), user[1].encode('utf-8')):
|
||||
return jsonify({"error": "Invalid email or password"}), 401
|
||||
|
||||
if user[2] != 'approved':
|
||||
return jsonify({"error": f"Membership status: {user[2]}"}), 403
|
||||
|
||||
token = jwt.encode({'user_id': user[0], 'exp': datetime.utcnow() + timedelta(days=30)}, JWT_SECRET, algorithm='HS256')
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('INSERT INTO sessions (user_id, session_token, expires_at) VALUES (?, ?, ?)',
|
||||
(user[0], token, datetime.utcnow() + timedelta(days=30)))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"token": token, "user_id": user[0], "message": "Login successful"}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Login failed"}), 500
|
||||
|
||||
@app.route('/api/auth/logout', methods=['POST'])
|
||||
@require_auth
|
||||
def logout():
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('DELETE FROM sessions WHERE session_token = ?', (token,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"message": "Logged out"}), 200
|
||||
|
||||
@app.route('/api/auth/check', methods=['GET'])
|
||||
def check_auth():
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
if not token: return jsonify({"authenticated": False}), 200
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT u.id, u.email, u.name, u.status FROM users u JOIN sessions s ON u.id = s.user_id WHERE s.session_token = ? AND s.expires_at > ?', (token, datetime.utcnow()))
|
||||
user = c.fetchone()
|
||||
conn.close()
|
||||
if user: return jsonify({"authenticated": True, "user_id": user[0], "email": user[1], "name": user[2], "status": user[3]}), 200
|
||||
except: pass
|
||||
return jsonify({"authenticated": False}), 200
|
||||
|
||||
@app.route('/api/download/count', methods=['GET'])
|
||||
def get_download_count():
|
||||
ip_address = get_client_ip()
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT COUNT(*) FROM downloads WHERE ip_address = ?', (ip_address,))
|
||||
count = c.fetchone()[0]
|
||||
conn.close()
|
||||
return jsonify({"count": count, "limit": 1, "unlimited": False}), 200
|
||||
|
||||
@app.route('/api/download/track', methods=['POST'])
|
||||
@rate_limit(max_per_minute=10, window_minutes=1)
|
||||
def track_download():
|
||||
data = request.get_json()
|
||||
file_path = sanitize_input(data.get('file_path', ''), max_length=500)
|
||||
ip_address = get_client_ip()
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('INSERT INTO downloads (ip_address, file_path) VALUES (?, ?)', (ip_address, file_path))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"message": "Tracked"}), 200
|
||||
|
||||
@app.route('/api/testimonials', methods=['GET', 'POST'])
|
||||
def handle_testimonials():
|
||||
if request.method == 'GET':
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT id, content, author_name, created_at FROM testimonials WHERE approved = 1 ORDER BY created_at DESC')
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
return jsonify({"testimonials": [{"id": r[0], "content": r[1], "author_name": r[2], "created_at": r[3]} for r in rows]}), 200
|
||||
else:
|
||||
@require_auth
|
||||
def submit():
|
||||
data = request.get_json()
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('INSERT INTO testimonials (user_id, content, author_name, approved) VALUES (?, ?, ?, 0)', (request.user_id, data.get('content'), data.get('author_name')))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"message": "Submitted"}), 201
|
||||
return submit()
|
||||
|
||||
@app.route('/api/contact', methods=['POST'])
|
||||
@require_auth
|
||||
def contact():
|
||||
data = request.get_json()
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('INSERT INTO contact_submissions (user_id, name, email, message) VALUES (?, ?, ?, ?)', (request.user_id, data.get('name'), data.get('email'), data.get('message')))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"message": "Sent"}), 200
|
||||
|
||||
@app.route('/api/admin/login', methods=['POST'])
|
||||
def admin_login():
|
||||
data = request.get_json()
|
||||
if data.get('password') == ADMIN_PASSWORD:
|
||||
token = jwt.encode({'admin': True, 'exp': datetime.utcnow() + timedelta(hours=24)}, JWT_SECRET, algorithm='HS256')
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('INSERT INTO admin_sessions (session_token, expires_at) VALUES (?, ?)', (token, datetime.utcnow() + timedelta(hours=24)))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"token": token}), 200
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
@app.route('/api/admin/pending', methods=['GET'])
|
||||
@require_admin
|
||||
def admin_pending():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT id, email, name, created_at FROM users WHERE status = "pending"')
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
return jsonify({"members": [{"id": r[0], "email": r[1], "name": r[2], "created_at": r[3]} for r in rows]}), 200
|
||||
|
||||
@app.route('/api/admin/approve', methods=['POST'])
|
||||
@require_admin
|
||||
def admin_approve():
|
||||
user_id = request.get_json().get('user_id')
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute('UPDATE users SET status = "approved" WHERE id = ?', (user_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"message": "Approved"}), 200
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health():
|
||||
return jsonify({"status":"healthy"}), 200
|
||||
|
||||
# ============================================================================
|
||||
# Static File Serving (CATCH-ALL)
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def serve(path):
|
||||
# Ignore API routes
|
||||
if path.startswith('api/'):
|
||||
return "Not Found", 404
|
||||
|
||||
full_path = os.path.join(STATIC_FOLDER, path)
|
||||
|
||||
# If path is a directory or empty, serve index.html
|
||||
if path == "" or os.path.isdir(full_path):
|
||||
# Check if the directory has an index.html
|
||||
if os.path.exists(os.path.join(full_path, 'index.html')):
|
||||
return send_from_directory(full_path, 'index.html')
|
||||
# Otherwise, check if it's a pretty URL like /about
|
||||
elif os.path.exists(os.path.join(STATIC_FOLDER, path, 'index.html')):
|
||||
return send_from_directory(os.path.join(STATIC_FOLDER, path), 'index.html')
|
||||
return send_from_directory(STATIC_FOLDER, '404.html'), 404
|
||||
|
||||
# If file exists, serve it
|
||||
if os.path.exists(full_path):
|
||||
return send_from_directory(STATIC_FOLDER, path)
|
||||
|
||||
# Handle Hugo's pretty URLs: /about -> /about/index.html
|
||||
if os.path.exists(os.path.join(STATIC_FOLDER, path, 'index.html')):
|
||||
return send_from_directory(os.path.join(STATIC_FOLDER, path), 'index.html')
|
||||
|
||||
# Default to 404
|
||||
if os.path.exists(os.path.join(STATIC_FOLDER, '404.html')):
|
||||
return send_from_directory(STATIC_FOLDER, '404.html'), 404
|
||||
return "404 Not Found", 404
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"Starting Membership API + Static Server on port {PORT}")
|
||||
app.run(host='0.0.0.0', port=PORT, debug=False)
|
||||
42
scripts/start_hugo_server.sh
Executable file
42
scripts/start_hugo_server.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Hugo Server Startup Script
|
||||
#
|
||||
# This script starts the Hugo development server with production-like settings.
|
||||
# It's designed to be run as a systemd service.
|
||||
#
|
||||
# Configuration:
|
||||
# - Site directory: /root/hydro-sterile
|
||||
# - Port: 1313 (Hugo default)
|
||||
# - Bind: 0.0.0.0 (all interfaces)
|
||||
# - Base URL: From hugo.yaml config
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SITE_DIR="/root/hydro-sterile"
|
||||
PORT="${HUGO_PORT:-7575}"
|
||||
BIND="${HUGO_BIND:-0.0.0.0}"
|
||||
BASEURL="${HUGO_BASEURL:-$(grep '^baseURL:' hugo.yaml | sed 's/.*: //' | tr -d "'\"")}"
|
||||
|
||||
cd "$SITE_DIR" || exit 1
|
||||
|
||||
# Check if Hugo is available via npm
|
||||
if command -v npm >/dev/null 2>&1 && [ -d "node_modules/.bin" ]; then
|
||||
HUGO_CMD="./node_modules/.bin/hugo"
|
||||
elif command -v hugo >/dev/null 2>&1; then
|
||||
HUGO_CMD="hugo"
|
||||
else
|
||||
echo "ERROR: Hugo not found. Please install Hugo or run 'npm install' in $SITE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start Hugo server
|
||||
# Using --noHTTPCache for development, remove for production
|
||||
exec "$HUGO_CMD" server \
|
||||
--bind "$BIND" \
|
||||
--port "$PORT" \
|
||||
--baseURL "$BASEURL" \
|
||||
--appendPort=false \
|
||||
--disableFastRender
|
||||
|
||||
403
static/js/membership.js
Normal file
403
static/js/membership.js
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Membership System Frontend
|
||||
* Handles authentication, paywall checks, and download tracking
|
||||
*/
|
||||
|
||||
const getApiBase = () => {
|
||||
const host = window.location.hostname;
|
||||
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
|
||||
return isLocal ? 'http://' + host + ':5001' : window.location.origin.replace(/\/$/, '');
|
||||
};
|
||||
const API_BASE = getApiBase() + '/api';
|
||||
|
||||
let authState = {
|
||||
authenticated: false,
|
||||
user: null,
|
||||
token: null
|
||||
};
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkAuthStatus();
|
||||
setupDownloadTracking();
|
||||
setupContactForm();
|
||||
});
|
||||
|
||||
/**
|
||||
* Check authentication status
|
||||
*/
|
||||
async function checkAuthStatus() {
|
||||
const token = localStorage.getItem('membership_token');
|
||||
if (!token) {
|
||||
updateAuthUI(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/check`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.authenticated) {
|
||||
authState = {
|
||||
authenticated: true,
|
||||
user: data,
|
||||
token: token
|
||||
};
|
||||
updateAuthUI(true);
|
||||
checkPaywallAccess();
|
||||
} else {
|
||||
localStorage.removeItem('membership_token');
|
||||
updateAuthUI(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
updateAuthUI(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI based on authentication state
|
||||
*/
|
||||
function updateAuthUI(authenticated) {
|
||||
const loginElements = document.querySelectorAll('.membership-login');
|
||||
const logoutElements = document.querySelectorAll('.membership-logout');
|
||||
const memberElements = document.querySelectorAll('.membership-only');
|
||||
const guestElements = document.querySelectorAll('.guest-only');
|
||||
|
||||
if (authenticated) {
|
||||
loginElements.forEach(el => el.style.display = 'none');
|
||||
logoutElements.forEach(el => el.style.display = 'block');
|
||||
memberElements.forEach(el => el.style.display = 'block');
|
||||
guestElements.forEach(el => el.style.display = 'none');
|
||||
|
||||
// Update user name display
|
||||
const userNameElements = document.querySelectorAll('.user-name');
|
||||
userNameElements.forEach(el => {
|
||||
if (authState.user && authState.user.name) {
|
||||
el.textContent = authState.user.name;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
loginElements.forEach(el => el.style.display = 'block');
|
||||
logoutElements.forEach(el => el.style.display = 'none');
|
||||
memberElements.forEach(el => el.style.display = 'none');
|
||||
guestElements.forEach(el => el.style.display = 'block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check paywall access and show/hide content
|
||||
*/
|
||||
async function checkPaywallAccess() {
|
||||
if (!authState.authenticated) {
|
||||
// Check download count for IP
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/download/count`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.limit_reached || (data.count >= data.limit && !data.unlimited)) {
|
||||
showPaywall();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download count check failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show paywall message
|
||||
*/
|
||||
function showPaywall() {
|
||||
const paywallElements = document.querySelectorAll('.paywall-message');
|
||||
paywallElements.forEach(el => {
|
||||
el.style.display = 'block';
|
||||
});
|
||||
|
||||
const paywallContent = document.querySelectorAll('.paywall-content');
|
||||
paywallContent.forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide paywall message
|
||||
*/
|
||||
function hidePaywall() {
|
||||
const paywallElements = document.querySelectorAll('.paywall-message');
|
||||
paywallElements.forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
const paywallContent = document.querySelectorAll('.paywall-content');
|
||||
paywallContent.forEach(el => {
|
||||
el.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup download tracking
|
||||
*/
|
||||
function setupDownloadTracking() {
|
||||
document.addEventListener('click', async function(e) {
|
||||
const downloadLink = e.target.closest('a[download], a[href*="/library/"], a[href*="/pdfs/"]');
|
||||
if (!downloadLink) return;
|
||||
|
||||
const filePath = downloadLink.getAttribute('href') || downloadLink.getAttribute('download');
|
||||
if (!filePath) return;
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!authState.authenticated) {
|
||||
// Check download count
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/download/count`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.count >= data.limit && !data.unlimited) {
|
||||
e.preventDefault();
|
||||
alert('You have reached the free download limit. Please sign up for membership to download unlimited resources.');
|
||||
window.location.href = '/membership/';
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Track download
|
||||
try {
|
||||
await fetch(`${API_BASE}/download/track`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authState.token ? { 'Authorization': `Bearer ${authState.token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({ file_path: filePath })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Download tracking failed:', error);
|
||||
// Don't prevent download if tracking fails
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup contact form
|
||||
*/
|
||||
function setupContactForm() {
|
||||
const contactForm = document.getElementById('contact-form');
|
||||
if (!contactForm) return;
|
||||
|
||||
contactForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!authState.authenticated) {
|
||||
alert('Please log in to submit the contact form.');
|
||||
window.location.href = '/membership/';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(contactForm);
|
||||
const data = {
|
||||
name: formData.get('name'),
|
||||
email: formData.get('email'),
|
||||
message: formData.get('message')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/contact`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authState.token}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert('Message sent successfully!');
|
||||
contactForm.reset();
|
||||
} else {
|
||||
alert(result.error || 'Failed to send message');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
alert('Failed to send message. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* User login
|
||||
*/
|
||||
async function login(email, password) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
localStorage.setItem('membership_token', data.token);
|
||||
authState = {
|
||||
authenticated: true,
|
||||
user: data,
|
||||
token: data.token
|
||||
};
|
||||
updateAuthUI(true);
|
||||
hidePaywall();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { success: false, error: data.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Login failed. Please try again.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User logout
|
||||
*/
|
||||
async function logout() {
|
||||
const token = localStorage.getItem('membership_token');
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`${API_BASE}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem('membership_token');
|
||||
authState = {
|
||||
authenticated: false,
|
||||
user: null,
|
||||
token: null
|
||||
};
|
||||
updateAuthUI(false);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* User registration
|
||||
*/
|
||||
async function register(email, password, name) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password, name })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return { success: true, message: data.message };
|
||||
} else {
|
||||
return { success: false, error: data.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Registration failed. Please try again.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit testimonial
|
||||
*/
|
||||
async function submitTestimonial(content, authorName) {
|
||||
if (!authState.authenticated) {
|
||||
return { success: false, error: 'Please log in to submit a testimonial' };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/testimonials`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authState.token}`
|
||||
},
|
||||
body: JSON.stringify({ content, author_name: authorName })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return { success: true, message: data.message };
|
||||
} else {
|
||||
return { success: false, error: data.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Failed to submit testimonial' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load testimonials
|
||||
*/
|
||||
async function loadTestimonials() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/testimonials`);
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return { success: true, testimonials: data.testimonials };
|
||||
} else {
|
||||
return { success: false, error: data.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Failed to load testimonials' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has reached their download limit (guests only)
|
||||
*/
|
||||
async function checkDownloadLimit() {
|
||||
if (authState.authenticated) {
|
||||
document.querySelectorAll('.paywall-message').forEach(el => el.style.display = 'none');
|
||||
document.querySelectorAll('.paywall-content').forEach(el => el.style.display = 'block');
|
||||
document.querySelectorAll('.guest-only').forEach(el => el.style.display = 'none');
|
||||
document.querySelectorAll('.membership-only').forEach(el => el.style.display = 'block');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/download/count`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.count >= data.limit && !data.unlimited) {
|
||||
document.querySelectorAll('.paywall-message').forEach(el => el.style.display = 'block');
|
||||
document.querySelectorAll('.paywall-content').forEach(el => el.style.display = 'none');
|
||||
} else {
|
||||
document.querySelectorAll('.paywall-message').forEach(el => el.style.display = 'none');
|
||||
document.querySelectorAll('.paywall-content').forEach(el => el.style.display = 'block');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download limit check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for use in Alpine.js or other scripts
|
||||
window.MembershipAPI = {
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
submitTestimonial,
|
||||
loadTestimonials,
|
||||
checkAuthStatus,
|
||||
checkDownloadLimit,
|
||||
getAuthState: () => authState
|
||||
};
|
||||
|
||||
0
static/pdfs/cleaning.pdf
Normal file
0
static/pdfs/cleaning.pdf
Normal file
0
static/pdfs/deficiencies.pdf
Normal file
0
static/pdfs/deficiencies.pdf
Normal file
0
static/pdfs/dutch-buckets.pdf
Normal file
0
static/pdfs/dutch-buckets.pdf
Normal file
0
static/pdfs/dwc-basics.pdf
Normal file
0
static/pdfs/dwc-basics.pdf
Normal file
0
static/pdfs/lighting.pdf
Normal file
0
static/pdfs/lighting.pdf
Normal file
0
static/pdfs/nft-guide.pdf
Normal file
0
static/pdfs/nft-guide.pdf
Normal file
0
static/pdfs/pests.pdf
Normal file
0
static/pdfs/pests.pdf
Normal file
0
static/pdfs/ph-ec-mastery.pdf
Normal file
0
static/pdfs/ph-ec-mastery.pdf
Normal file
0
static/pdfs/seeds.pdf
Normal file
0
static/pdfs/seeds.pdf
Normal file
0
static/pdfs/sterile-res.pdf
Normal file
0
static/pdfs/sterile-res.pdf
Normal file
5
static/robots.txt
Normal file
5
static/robots.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://hydrolord.thetempleofdoom.com/sitemap.xml
|
||||
|
||||
Reference in New Issue
Block a user