From 53ca792988fe0f56a985281bafd72b886f986350 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Mon, 8 Dec 2025 08:12:35 +0200 Subject: [PATCH] Style register and sign in and create confirmation when sign out --- backend/__pycache__/main.cpython-313.pyc | Bin 12929 -> 14485 bytes .../__pycache__/user_db_utils.cpython-313.pyc | Bin 3938 -> 4298 bytes backend/main.py | 36 ++++++++-- backend/schema.sql | 3 + backend/user_db_utils.py | 19 +++--- frontend/index.html | 2 +- frontend/src/App.css | 21 ++++-- frontend/src/App.jsx | 25 +++++-- frontend/src/authApi.js | 11 ++- frontend/src/components/Register.jsx | 64 +++++++++++++++--- 10 files changed, 147 insertions(+), 34 deletions(-) diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 72cc141fc577ae13b652683862a703a12fcc484c..be7bf31d86b19d834559fed170cff68a6f31f566 100644 GIT binary patch delta 4769 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 VywEnfE`hY($}dbr9r*vwe*tNPk@5fl diff --git a/backend/__pycache__/user_db_utils.cpython-313.pyc b/backend/__pycache__/user_db_utils.cpython-313.pyc index e6126eeeca5d51376a447906e9cbc616a9c115e6..c500bba03ee96a69ad65e65a3b506701eacd650e 100644 GIT binary patch delta 1119 zcmb7C%}*0S6rX8#r@K4d7BEyOuq;$8g%Z#LN}R<3tp6i|A2Do!HXs)#uJGbQetA%c=2G7n0PSJH~m1o;Uw?(=J!7Ky*F<^8gJYD z`?{_nIL~JW(|5z}vr zP{El)L<4h{m=e)rDlk`xX*Y17=Gl6KeWakxq?xriosG|>3Th@^y*iU=>sBVdQgS3n zwJFdlh#h4A8h>eTu#+g^hQrt7Fi=$Pti;7_2-syDMOh{4;J;OmX9`dGrY8Ey0Jy|4 zQPeT1@-f{@vTBq}Qn)trS&3400GC4`GbgoT!Z#%!uHJK!!M|xLr?MaaRe&j=O+Y(9K}yVLvjux%i52Q* z(%E=s`fx>72Qik|uX%7-lgOh9mWnT?rkAV~W4&Oh6_F1Ba_BQ{en*Ym`5n#0gQ~lu zR<8fjGxR8yzrC$^KPYsK-ls*Iq}LQR-lQC^vL$9><@!-~1 zlELHz81Jm`$MCeh)P1DMN2tM{DxU6>kjliOnO}fdE$b4{4G@sZZxa-T9=@j^=f4z> zc3QYVtQFp@21wC{^aL@`L;n`LVBiXrgz%TL^S083P>j3+ zCft2~f0SjCYZ`i`?%~*LR{|vgfJC j^&7q-0=1baseGBe36cyDx(0HNYKQQ&+)vG7GH4qQ#Kd=gSebZ9X#7X9R@B7})_ufqQ>-f)b>_pd91nW&^ zu98y_`o&3aNoUE)qj$0RqL?QZfi2}pO0wJ`^i-sh1!~G=$sOkAbzn+|$(EW|fO$Gh zeT);@$Ah3SR1|vt5P1vEPnOt@9ux#yV16+|#4xf&c2^7rY*$K{b#+geAg(Lyn;epA z#1_^)kd7J;^llX)o6O>F4u}0@41*a7vkfo7Jw8LS_<+46X(OSUee2%Bs-3aSI3v=hP`C4EqmU2UV9vu zs1G%MMd7E}!XI22ywtjua=F=tjvlO}5t zlWl6Ftf?3i5vWrBPV_Q|D2Et_I6y+|&YIvtlKoJo)T@wQfn9$FGbQAJnb}oMIIK*1o1$72WbLXx?TYJdZ4``YAu18NGirT#W|*au%$oCb;g_D${a z2dDgoln+FM9QYYJ%pt^K1mH-%QQa=>Zk5w?9w`4a>T3XwfH6Ks$}#eNN69~uj3vhi h;K4@3K@gh7PWT)-<;+RyAYDI25F8VrVX;pG#y`-+s4oBj diff --git a/backend/main.py b/backend/main.py index e6a755e..a52d7db 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ from datetime import timedelta from fastapi import FastAPI, HTTPException, Query, Depends from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, field_validator import os import uvicorn @@ -62,6 +62,18 @@ class UserRegister(BaseModel): username: str email: EmailStr password: str + first_name: Optional[str] = None + last_name: Optional[str] = None + display_name: str + + @field_validator('username') + @classmethod + def username_must_be_english(cls, v: str) -> str: + if not v.isascii(): + raise ValueError('שם משתמש חייב להיות באנגלית בלבד') + if not all(c.isalnum() or c in '_-' for c in v): + raise ValueError('שם משתמש יכול להכיל רק אותיות, מספרים, _ ו-') + return v class UserLogin(BaseModel): @@ -78,6 +90,9 @@ class UserResponse(BaseModel): id: int username: str email: str + first_name: Optional[str] = None + last_name: Optional[str] = None + display_name: str app = FastAPI( @@ -277,13 +292,23 @@ def register(user: UserRegister): print(f"[REGISTER] Hashing password...") password_hash = hash_password(user.password) print(f"[REGISTER] Creating user in database...") - new_user = create_user(user.username, user.email, password_hash) + new_user = create_user( + user.username, + user.email, + password_hash, + user.first_name, + user.last_name, + user.display_name + ) print(f"[REGISTER] User created successfully: {new_user['id']}") return UserResponse( id=new_user["id"], username=new_user["username"], - email=new_user["email"] + email=new_user["email"], + first_name=new_user.get("first_name"), + last_name=new_user.get("last_name"), + display_name=new_user["display_name"] ) @@ -326,7 +351,10 @@ def get_me(current_user: dict = Depends(get_current_user)): return UserResponse( id=user["id"], username=user["username"], - email=user["email"] + email=user["email"], + first_name=user.get("first_name"), + last_name=user.get("last_name"), + display_name=user["display_name"] ) diff --git a/backend/schema.sql b/backend/schema.sql index 76916b5..88bfd34 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -4,6 +4,9 @@ CREATE TABLE IF NOT EXISTS users ( username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, + first_name TEXT, + last_name TEXT, + display_name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/backend/user_db_utils.py b/backend/user_db_utils.py index fd37d36..264da28 100644 --- a/backend/user_db_utils.py +++ b/backend/user_db_utils.py @@ -14,18 +14,21 @@ def get_db_connection(): ) -def create_user(username: str, email: str, password_hash: str): +def create_user(username: str, email: str, password_hash: str, first_name: str = None, last_name: str = None, display_name: str = None): """Create a new user""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: + # Use display_name if provided, otherwise use username + final_display_name = display_name if display_name else username + cur.execute( """ - INSERT INTO users (username, email, password_hash) - VALUES (%s, %s, %s) - RETURNING id, username, email, created_at + INSERT INTO users (username, email, password_hash, first_name, last_name, display_name) + VALUES (%s, %s, %s, %s, %s, %s) + RETURNING id, username, email, first_name, last_name, display_name, created_at """, - (username, email, password_hash) + (username, email, password_hash, first_name, last_name, final_display_name) ) user = cur.fetchone() conn.commit() @@ -41,7 +44,7 @@ def get_user_by_username(username: str): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, password_hash, created_at FROM users WHERE username = %s", + "SELECT id, username, email, password_hash, first_name, last_name, display_name, created_at FROM users WHERE username = %s", (username,) ) user = cur.fetchone() @@ -57,7 +60,7 @@ def get_user_by_email(email: str): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, password_hash, created_at FROM users WHERE email = %s", + "SELECT id, username, email, password_hash, first_name, last_name, display_name, created_at FROM users WHERE email = %s", (email,) ) user = cur.fetchone() @@ -73,7 +76,7 @@ def get_user_by_id(user_id: int): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, created_at FROM users WHERE id = %s", + "SELECT id, username, email, first_name, last_name, display_name, created_at FROM users WHERE id = %s", (user_id,) ) user = cur.fetchone() diff --git a/frontend/index.html b/frontend/index.html index 259fcb7..577d29f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - My Recipes | מתכונים שלי + My Recipes | המתכונים שלי diff --git a/frontend/src/App.css b/frontend/src/App.css index 4780b9d..b88d521 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -765,7 +765,7 @@ body { } [data-theme="light"] body { - background: linear-gradient(180deg, #ac8d75 0%, #f6f8fa 100%); + background: linear-gradient(180deg, #ac8d75 0%, #ede9e5 100%); color: var(--text-main); } @@ -1292,17 +1292,17 @@ html { align-items: center; justify-content: center; padding: 2rem; - background: var(--bg); + background: radial-gradient(circle at top, #0f172a 0, #020617 55%); } .auth-card { width: 100%; max-width: 420px; - background: var(--card); - border: 1px solid var(--border); + background: #020617; + border: 1px solid var(--border-subtle); border-radius: 16px; padding: 2rem; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7); } .auth-title { @@ -1349,3 +1349,14 @@ html { .full-width { width: 100%; } + +/* Light mode auth styles */ +[data-theme="light"] .auth-container { + background: linear-gradient(180deg, #ac8d75 0%, #ede9e5 100%); +} + +[data-theme="light"] .auth-card { + background: #d1b29b; + border: 1px solid rgba(107, 114, 128, 0.2); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5948a98..467216d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -41,6 +41,7 @@ function App() { const [editingRecipe, setEditingRecipe] = useState(null); const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" }); + const [logoutModal, setLogoutModal] = useState(false); const [toasts, setToasts] = useState([]); const [theme, setTheme] = useState(() => { try { @@ -249,11 +250,17 @@ function App() { }; const handleLogout = () => { + setLogoutModal(true); + }; + + const confirmLogout = () => { removeToken(); setUser(null); setIsAuthenticated(false); setRecipes([]); setSelectedRecipe(null); + setLogoutModal(false); + addToast('התנתקת בהצלחה', 'success'); }; // Show loading state while checking auth @@ -275,7 +282,7 @@ function App() { {/* User greeting above TopBar */} {isAuthenticated && user && (
- שלום, {user.username} 👋 + שלום, {user.display_name || user.username} 👋
)} @@ -313,10 +320,7 @@ function App() { /> ) : ( { - addToast("נרשמת בהצלחה! כעת התחבר", "success"); - setAuthView("login"); - }} + onSuccess={handleLoginSuccess} onSwitchToLogin={() => setAuthView("login")} /> )} @@ -430,6 +434,17 @@ function App() { onCancel={handleCancelDelete} /> + setLogoutModal(false)} + /> + ); diff --git a/frontend/src/authApi.js b/frontend/src/authApi.js index 626dd1d..e6ae1dc 100644 --- a/frontend/src/authApi.js +++ b/frontend/src/authApi.js @@ -8,11 +8,18 @@ const getApiBase = () => { const API_BASE = getApiBase(); -export async function register(username, email, password) { +export async function register(username, email, password, firstName, lastName, displayName) { const res = await fetch(`${API_BASE}/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, email, password }), + body: JSON.stringify({ + username, + email, + password, + first_name: firstName, + last_name: lastName, + display_name: displayName + }), }); if (!res.ok) { const error = await res.json(); diff --git a/frontend/src/components/Register.jsx b/frontend/src/components/Register.jsx index 01d197f..40d1bdc 100644 --- a/frontend/src/components/Register.jsx +++ b/frontend/src/components/Register.jsx @@ -1,11 +1,14 @@ import { useState } from "react"; -import { register } from "../authApi"; +import { register, login, saveToken } from "../authApi"; function Register({ onSuccess, onSwitchToLogin }) { const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [displayName, setDisplayName] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -24,12 +27,21 @@ function Register({ onSuccess, onSwitchToLogin }) { return; } + if (!displayName.trim()) { + setError("שם תצוגה הוא שדה חובה"); + return; + } + setLoading(true); try { - await register(username, email, password); - // After successful registration, switch to login - onSwitchToLogin(); + // Register the user + await register(username, email, password, firstName, lastName, displayName); + + // Automatically login after successful registration + const response = await login(username, password); + saveToken(response.access_token); + onSuccess(); } catch (err) { setError(err.message); } finally { @@ -47,20 +59,54 @@ function Register({ onSuccess, onSwitchToLogin }) { {error &&
{error}
}
- + + setFirstName(e.target.value)} + placeholder="שם פרטי (אופציונלי)" + /> +
+ +
+ + setLastName(e.target.value)} + placeholder="שם משפחה (אופציונלי)" + /> +
+ +
+ + setDisplayName(e.target.value)} + required + placeholder="איך תרצה שיופיע שמך?" + minLength={2} + /> +
+ +
+ setUsername(e.target.value)} required - placeholder="בחר שם משתמש" + placeholder="username (English only)" autoComplete="username" minLength={3} + pattern="[a-zA-Z0-9_-]+" + title="שם משתמש יכול להכיל רק אותיות באנגלית, מספרים, _ ו-" />
- +
- +
- +