Files changed (4) hide show
  1. app.py +10 -193
  2. index.html +131 -0
  3. requirements.txt +0 -1
  4. templates/index.html +0 -155
app.py CHANGED
@@ -1,200 +1,17 @@
1
- import os
2
- import json
3
- import time
4
- import pandas as pd # Included as requested
5
- from flask import Flask, render_template, request, redirect, url_for, session, flash
6
- from huggingface_hub import HfApi, hf_hub_download
7
- from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError
8
- from werkzeug.security import generate_password_hash, check_password_hash
9
- from threading import Lock
10
- from dotenv import load_dotenv
11
-
12
- # Load local .env file if present (Good for local testing)
13
- load_dotenv()
14
-
15
- app = Flask(__name__)
16
- app.secret_key = os.environ.get("SECRET_KEY", "super_secret_key_123")
17
-
18
- # --- CONFIGURATION (WITH AUTO-FIX) ---
19
- # We use .strip() here to automatically remove invisible spaces
20
- # that often happen when pasting into Space Settings.
21
- token_env = os.environ.get("HF_TOKEN")
22
- HF_TOKEN = token_env.strip() if token_env else None
23
-
24
- repo_env = os.environ.get("DB_REPO")
25
- DB_REPO = repo_env.strip() if repo_env else None
26
-
27
- DB_FILE = "db.json"
28
-
29
- # Print to logs so you can verify exactly what the app sees
30
- print(f"--- SYSTEM CONFIG ---")
31
- print(f"Target Repo: '{DB_REPO}'")
32
- print(f"Token Found: {'Yes' if HF_TOKEN else 'NO (Check Secrets!)'}")
33
- print(f"---------------------")
34
-
35
- # Initialize API
36
- if HF_TOKEN:
37
- api = HfApi(token=HF_TOKEN)
38
- else:
39
- api = None
40
- print("WARNING: No HF_TOKEN found. App is strictly Read-Only and may crash on save.")
41
-
42
- db_lock = Lock()
43
-
44
- # --- STARTER DATA ---
45
- DEFAULT_PINS = [
46
- {"id": 1, "url": "https://images.unsplash.com/photo-1541963463532-d68292c34b19", "caption": "Nature Vibes", "author": "System"},
47
- {"id": 2, "url": "https://images.unsplash.com/photo-1493246507139-91e8fad9978e", "caption": "Alpine Lake", "author": "System"},
48
- {"id": 3, "url": "https://images.unsplash.com/photo-1511497584788-876760111969", "caption": "Forest Mist", "author": "System"},
49
- {"id": 4, "url": "https://images.unsplash.com/photo-1682687982501-1e58ab814714", "caption": "Desert Life", "author": "System"},
50
- {"id": 5, "url": "https://images.unsplash.com/photo-1472214103451-9374bd1c798e", "caption": "Green Valley", "author": "System"}
51
- ]
52
-
53
  def load_db():
54
- """
55
- Tries to download the DB. If it fails because the file doesn't exist yet,
56
- it returns the default data so the app can start (and save it later).
57
- """
58
  if not HF_TOKEN or not DB_REPO:
59
- print("WARNING: HF_TOKEN or DB_REPO secrets are missing. Site is Read-Only.")
60
- return {"users": {}, "pins": DEFAULT_PINS}
61
 
62
  try:
63
- print(f"Attempting to download {DB_FILE} from {DB_REPO}...")
64
  path = hf_hub_download(repo_id=DB_REPO, filename=DB_FILE, repo_type="dataset", token=HF_TOKEN)
65
-
66
  with open(path, 'r') as f:
67
- data = json.load(f)
68
- # Ensure structure exists
69
- if 'pins' not in data: data['pins'] = DEFAULT_PINS
70
- if 'users' not in data: data['users'] = {}
71
- print("Database loaded successfully.")
72
- return data
73
-
74
- except (EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError) as e:
75
- print(f"NOTE: Could not load {DB_FILE}. Reason: {e}")
76
- print("Initializing with default data (Normal for first run).")
77
- return {"users": {}, "pins": DEFAULT_PINS}
78
-
79
  except Exception as e:
80
- print(f"CRITICAL: Unknown DB Load Error: {e}")
81
- return {"users": {}, "pins": DEFAULT_PINS}
82
-
83
- def save_db(data):
84
- """
85
- Saves the DB locally and then pushes to Hugging Face.
86
- """
87
- if not HF_TOKEN or not DB_REPO:
88
- flash("Error: Cannot save. Secrets (HF_TOKEN/DB_REPO) are missing!")
89
- return False
90
-
91
- with db_lock:
92
- try:
93
- # 1. Save locally
94
- with open(DB_FILE, 'w') as f:
95
- json.dump(data, f, indent=2)
96
-
97
- # 2. Push to Hugging Face Dataset
98
- if api:
99
- print("Syncing to Hugging Face Dataset...")
100
- api.upload_file(
101
- path_or_fileobj=DB_FILE,
102
- path_in_repo=DB_FILE,
103
- repo_id=DB_REPO,
104
- repo_type="dataset",
105
- commit_message="Sync DB via App"
106
- )
107
- print("Sync successful!")
108
- return True
109
- else:
110
- flash("Error: API not initialized (Missing Token)")
111
- return False
112
-
113
- except Exception as e:
114
- print(f"Sync Error: {e}")
115
- flash(f"Database Sync Failed: {str(e)}")
116
- return False
117
-
118
- # Load data on startup
119
- DATA_CACHE = load_db()
120
-
121
- # --- ROUTES ---
122
- @app.route('/')
123
- def index():
124
- return render_template('index.html', pins=DATA_CACHE.get('pins', []), user=session.get('user'))
125
-
126
- @app.route('/signup', methods=['POST'])
127
- def signup():
128
- username = request.form.get('username')
129
- password = request.form.get('password')
130
-
131
- if not username or not password:
132
- flash("Username and password required")
133
- return redirect(url_for('index'))
134
-
135
- # 1. Check if user exists
136
- if username in DATA_CACHE.get('users', {}):
137
- flash("User already exists!")
138
- return redirect(url_for('index'))
139
-
140
- # 2. Add user
141
- DATA_CACHE.setdefault('users', {})[username] = generate_password_hash(password)
142
-
143
- # 3. Save to DB
144
- success = save_db(DATA_CACHE)
145
- if success:
146
- session['user'] = username
147
- flash("Account created successfully!")
148
- else:
149
- # Revert change if save failed
150
- del DATA_CACHE['users'][username]
151
-
152
- return redirect(url_for('index'))
153
-
154
- @app.route('/login', methods=['POST'])
155
- def login():
156
- username = request.form.get('username')
157
- password = request.form.get('password')
158
- users = DATA_CACHE.get('users', {})
159
-
160
- if username in users and check_password_hash(users[username], password):
161
- session['user'] = username
162
- flash("Logged in!")
163
- else:
164
- # Debug helper: Print what we checked against
165
- print(f"Login Failed for {username}. Available users: {list(users.keys())}")
166
- flash("Invalid username or password")
167
-
168
- return redirect(url_for('index'))
169
-
170
- @app.route('/logout')
171
- def logout():
172
- session.pop('user', None)
173
- flash("Logged out")
174
- return redirect(url_for('index'))
175
-
176
- @app.route('/add_pin', methods=['POST'])
177
- def add_pin():
178
- if 'user' not in session: return redirect(url_for('index'))
179
-
180
- img_url = request.form.get('img_url')
181
- caption = request.form.get('caption')
182
-
183
- if not img_url:
184
- flash("Image URL is required")
185
- return redirect(url_for('index'))
186
-
187
- new_pin = {
188
- "id": int(time.time()),
189
- "url": img_url,
190
- "caption": caption,
191
- "author": session['user']
192
- }
193
-
194
- DATA_CACHE.setdefault('pins', []).insert(0, new_pin)
195
- save_db(DATA_CACHE)
196
- flash("Pin added!")
197
- return redirect(url_for('index'))
198
-
199
- if __name__ == '__main__':
200
- app.run(host='0.0.0.0', port=7860)
 
1
+ # --- REPLACEMENT FOR load_db FUNCTION ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  def load_db():
3
+ # 1. Check if variables are set
 
 
 
4
  if not HF_TOKEN or not DB_REPO:
5
+ print("ERROR: HF_TOKEN or DB_REPO secrets are missing!")
6
+ return {"users": {}, "pins": []}
7
 
8
  try:
9
+ # 2. Try to download existing DB
10
  path = hf_hub_download(repo_id=DB_REPO, filename=DB_FILE, repo_type="dataset", token=HF_TOKEN)
 
11
  with open(path, 'r') as f:
12
+ return json.load(f)
 
 
 
 
 
 
 
 
 
 
 
13
  except Exception as e:
14
+ print(f"Database not found or error loading: {e}")
15
+ # 3. If it fails, assume it's a new app and start fresh
16
+ return {"users": {}, "pins": []}
17
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Visual Board</title>
7
+ <style>
8
+ /* --- RESET & BASIC --- */
9
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #fafafa; }
10
+ nav { padding: 15px; background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100;}
11
+
12
+ /* --- MASONRY LAYOUT (The Pinterest Magic) --- */
13
+ .gallery {
14
+ column-count: 5; /* Default columns */
15
+ column-gap: 15px;
16
+ padding: 15px;
17
+ max-width: 1600px;
18
+ margin: 0 auto;
19
+ }
20
+
21
+ .pin {
22
+ break-inside: avoid;
23
+ margin-bottom: 15px;
24
+ border-radius: 16px;
25
+ overflow: hidden;
26
+ position: relative;
27
+ background: white;
28
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
29
+ transition: transform 0.2s;
30
+ }
31
+ .pin:hover { transform: translateY(-2px); box-shadow: 0 5px 12px rgba(0,0,0,0.2); }
32
+ .pin img { width: 100%; display: block; height: auto; }
33
+ .pin-info { padding: 10px; font-size: 0.9rem; color: #333; }
34
+
35
+ /* --- RESPONSIVE --- */
36
+ @media (max-width: 1200px) { .gallery { column-count: 4; } }
37
+ @media (max-width: 900px) { .gallery { column-count: 3; } }
38
+ @media (max-width: 600px) { .gallery { column-count: 2; } }
39
+
40
+ /* --- UI ELEMENTS --- */
41
+ .btn { padding: 8px 16px; border-radius: 20px; border: none; cursor: pointer; font-weight: bold; }
42
+ .btn-red { background: #e60023; color: white; }
43
+ .btn-grey { background: #efefef; color: black; }
44
+
45
+ .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
46
+ .modal-content { background: white; padding: 30px; border-radius: 16px; width: 300px; text-align: center; }
47
+ input { width: 90%; padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 8px; }
48
+
49
+ /* Show modal helper */
50
+ .show { display: flex !important; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+
55
+ <nav>
56
+ <div style="font-weight:bold; color:#e60023; font-size:1.2rem;">📌 PinSpace</div>
57
+ <div>
58
+ {% if user %}
59
+ <span style="margin-right:10px;">Hi, {{ user }}</span>
60
+ <button class="btn btn-grey" onclick="toggleModal('uploadModal')">+ Create</button>
61
+ <a href="/logout"><button class="btn btn-grey">Logout</button></a>
62
+ {% else %}
63
+ <button class="btn btn-red" onclick="toggleModal('loginModal')">Log in</button>
64
+ <button class="btn btn-grey" onclick="toggleModal('signupModal')">Sign up</button>
65
+ {% endif %}
66
+ </div>
67
+ </nav>
68
+
69
+ {% with messages = get_flashed_messages() %}
70
+ {% if messages %}
71
+ <div style="text-align:center; padding:10px; background:#fff3cd;">{{ messages[0] }}</div>
72
+ {% endif %}
73
+ {% endwith %}
74
+
75
+ <div class="gallery">
76
+ {% for pin in pins %}
77
+ <div class="pin">
78
+ <img src="{{ pin.url }}" onerror="this.src='https://via.placeholder.com/300?text=Broken+Link'">
79
+ <div class="pin-info">
80
+ <strong>{{ pin.caption }}</strong><br>
81
+ <span style="color:#777; font-size:0.8rem">by {{ pin.author }}</span>
82
+ </div>
83
+ </div>
84
+ {% endfor %}
85
+ </div>
86
+
87
+ <div id="loginModal" class="modal">
88
+ <div class="modal-content">
89
+ <h2>Login</h2>
90
+ <form action="/login" method="POST">
91
+ <input type="text" name="username" placeholder="Username" required>
92
+ <input type="password" name="password" placeholder="Password" required>
93
+ <br><br><button class="btn btn-red" type="submit">Log in</button>
94
+ </form>
95
+ <p onclick="toggleModal('loginModal')" style="cursor:pointer; font-size:0.8rem; color:#777;">Close</p>
96
+ </div>
97
+ </div>
98
+
99
+ <div id="signupModal" class="modal">
100
+ <div class="modal-content">
101
+ <h2>Sign Up</h2>
102
+ <form action="/signup" method="POST">
103
+ <input type="text" name="username" placeholder="Username" required>
104
+ <input type="password" name="password" placeholder="Password" required>
105
+ <br><br><button class="btn btn-red" type="submit">Create Account</button>
106
+ </form>
107
+ <p onclick="toggleModal('signupModal')" style="cursor:pointer; font-size:0.8rem; color:#777;">Close</p>
108
+ </div>
109
+ </div>
110
+
111
+ <div id="uploadModal" class="modal">
112
+ <div class="modal-content">
113
+ <h2>Add a Pin</h2>
114
+ <form action="/add_pin" method="POST">
115
+ <input type="text" name="img_url" placeholder="Image URL (e.g. https://...)" required>
116
+ <input type="text" name="caption" placeholder="Caption" required>
117
+ <br><br><button class="btn btn-red" type="submit">Post</button>
118
+ </form>
119
+ <p onclick="toggleModal('uploadModal')" style="cursor:pointer; font-size:0.8rem; color:#777;">Close</p>
120
+ </div>
121
+ </div>
122
+
123
+ <script>
124
+ function toggleModal(id) {
125
+ const el = document.getElementById(id);
126
+ el.classList.toggle('show');
127
+ }
128
+ </script>
129
+
130
+ </body>
131
+ </html>
requirements.txt CHANGED
@@ -2,4 +2,3 @@ flask
2
  huggingface_hub
3
  werkzeug
4
  pandas
5
- python-dotenv
 
2
  huggingface_hub
3
  werkzeug
4
  pandas
 
templates/index.html DELETED
@@ -1,155 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Visual Board</title>
7
- <style>
8
- /* --- CORE STYLES --- */
9
- body { margin: 0; font-family: sans-serif; background: #fafafa; padding-top: 80px; }
10
-
11
- /* NAVBAR */
12
- nav {
13
- position: fixed; top: 0; left: 0; right: 0; height: 65px;
14
- background: white; display: flex; align-items: center; justify-content: space-between;
15
- padding: 0 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); z-index: 1000;
16
- }
17
- .logo { color: #e60023; font-weight: bold; font-size: 1.2rem; display: flex; align-items: center; gap: 5px; }
18
- .user-greeting { margin-right: 15px; font-weight: 600; color: #333; }
19
-
20
- /* BUTTONS */
21
- .btn { padding: 10px 16px; border-radius: 24px; border: none; font-weight: bold; cursor: pointer; font-size: 0.9rem; transition: 0.2s; }
22
- .btn-red { background: #e60023; color: white; }
23
- .btn-grey { background: #efefef; color: black; }
24
- .btn:hover { opacity: 0.85; transform: scale(1.02); }
25
-
26
- /* PINS / MASONRY */
27
- .gallery {
28
- column-count: 5; column-gap: 15px; padding: 10px; max-width: 1400px; margin: 0 auto;
29
- }
30
- .pin {
31
- break-inside: avoid; margin-bottom: 15px; border-radius: 16px; overflow: hidden;
32
- background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: 0.2s;
33
- }
34
- .pin:hover { box-shadow: 0 5px 15px rgba(0,0,0,0.2); }
35
- .pin img { width: 100%; display: block; }
36
- .pin-info { padding: 10px 12px; font-size: 0.85rem; }
37
- .pin-author { color: #777; font-size: 0.8rem; margin-top: 4px; display: block; }
38
-
39
- /* RESPONSIVE COLUMNS */
40
- @media (max-width: 1200px) { .gallery { column-count: 4; } }
41
- @media (max-width: 900px) { .gallery { column-count: 3; } }
42
- @media (max-width: 600px) { .gallery { column-count: 2; padding: 5px; } nav { padding: 0 10px; } .user-greeting { display: none; } }
43
-
44
- /* MODALS (Login/Signup) */
45
- .modal {
46
- display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
47
- background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center;
48
- backdrop-filter: blur(2px);
49
- }
50
- .modal.show { display: flex; }
51
- .modal-content {
52
- background: white; padding: 30px; border-radius: 24px; width: 90%; max-width: 380px;
53
- text-align: center; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
54
- }
55
- .modal input {
56
- width: 100%; padding: 12px; margin: 8px 0; border: 2px solid #ddd;
57
- border-radius: 12px; box-sizing: border-box; font-size: 1rem;
58
- }
59
- .modal input:focus { border-color: #e60023; outline: none; }
60
-
61
- /* ALERT BAR */
62
- .alert {
63
- background: #333; color: white; text-align: center; padding: 12px;
64
- position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
65
- border-radius: 50px; z-index: 3000; min-width: 300px;
66
- box-shadow: 0 5px 15px rgba(0,0,0,0.3); animation: popUp 0.3s ease-out;
67
- }
68
- @keyframes popUp { from { transform: translate(-50%, 100%); } to { transform: translate(-50%, 0); } }
69
- </style>
70
- </head>
71
- <body>
72
-
73
- <nav>
74
- <div class="logo">📌 PinSpace</div>
75
- <div style="display:flex; align-items:center;">
76
- {% if user %}
77
- <span class="user-greeting">Hi, {{ user }}</span>
78
- <button class="btn btn-grey" onclick="openModal('uploadModal')" style="margin-right: 10px;">+ Add Pin</button>
79
- <a href="/logout" style="text-decoration:none;"><button class="btn btn-red">Log Out</button></a>
80
- {% else %}
81
- <button class="btn btn-red" onclick="openModal('loginModal')" style="margin-right: 10px;">Log in</button>
82
- <button class="btn btn-grey" onclick="openModal('signupModal')">Sign up</button>
83
- {% endif %}
84
- </div>
85
- </nav>
86
-
87
- {% with messages = get_flashed_messages() %}
88
- {% if messages %}
89
- <div class="alert" onclick="this.style.display='none'">{{ messages[0] }}</div>
90
- {% endif %}
91
- {% endwith %}
92
-
93
- <div class="gallery">
94
- {% for pin in pins %}
95
- <div class="pin">
96
- <img src="{{ pin.url }}" loading="lazy" alt="Pin">
97
- <div class="pin-info">
98
- <strong>{{ pin.caption }}</strong>
99
- <span class="pin-author">by {{ pin.author }}</span>
100
- </div>
101
- </div>
102
- {% endfor %}
103
- </div>
104
-
105
- <div id="loginModal" class="modal" onclick="closeModal(event, 'loginModal')">
106
- <div class="modal-content">
107
- <h2 style="margin-top:0">Welcome Back</h2>
108
- <form action="/login" method="POST">
109
- <input type="text" name="username" placeholder="Username" required>
110
- <input type="password" name="password" placeholder="Password" required>
111
- <br><br>
112
- <button class="btn btn-red" style="width:100%">Log In</button>
113
- </form>
114
- </div>
115
- </div>
116
-
117
- <div id="signupModal" class="modal" onclick="closeModal(event, 'signupModal')">
118
- <div class="modal-content">
119
- <h2 style="margin-top:0">Join PinSpace</h2>
120
- <form action="/signup" method="POST">
121
- <input type="text" name="username" placeholder="Choose Username" required>
122
- <input type="password" name="password" placeholder="Password" required>
123
- <br><br>
124
- <button class="btn btn-red" style="width:100%">Create Account</button>
125
- </form>
126
- </div>
127
- </div>
128
-
129
- <div id="uploadModal" class="modal" onclick="closeModal(event, 'uploadModal')">
130
- <div class="modal-content">
131
- <h2 style="margin-top:0">Add Image</h2>
132
- <form action="/add_pin" method="POST">
133
- <input type="text" name="img_url" placeholder="Paste Image URL here..." required>
134
- <input type="text" name="caption" placeholder="Caption / Title" required>
135
- <br><br>
136
- <button class="btn btn-grey" style="width:100%">Post Pin</button>
137
- </form>
138
- <p style="font-size:0.8rem; color:#888; margin-bottom:0">Paste a link ending in .jpg or .png</p>
139
- </div>
140
- </div>
141
-
142
- <script>
143
- function openModal(id) {
144
- document.getElementById(id).classList.add('show');
145
- }
146
- function closeModal(e, id) {
147
- // Close if clicking outside the white box (the dark overlay)
148
- if(e.target.id === id) {
149
- document.getElementById(id).classList.remove('show');
150
- }
151
- }
152
- </script>
153
-
154
- </body>
155
- </html>