added info to meal
This commit is contained in:
@@ -1,76 +1,123 @@
|
|||||||
MEAL ENHANCEMENTS - WORKING!
|
EDIT MEAL FEATURE - IMPLEMENTED
|
||||||
|
|
||||||
=== ✅ IMPLEMENTATION COMPLETE ===
|
=== ✅ NEW FEATURE ===
|
||||||
|
|
||||||
Meals now have:
|
You can now EDIT existing meals!
|
||||||
1. Instructions (multi-line)
|
|
||||||
2. Prep time (minutes)
|
|
||||||
3. Image (URL)
|
|
||||||
|
|
||||||
=== IF MEALS/WEEK PLAN TABS DON'T LOAD ===
|
Click "Edit" button → Modal opens → Make changes → Save
|
||||||
|
|
||||||
Your database needs the new columns!
|
=== HOW IT WORKS ===
|
||||||
|
|
||||||
SOLUTION - Option 1 (Fresh start):
|
1. Click "Edit" button on any meal
|
||||||
rm mealprep.db
|
2. Modal dialog opens with form
|
||||||
./start.sh
|
3. All fields pre-filled with current values:
|
||||||
|
|
||||||
SOLUTION - Option 2 (Keep data):
|
|
||||||
Just restart the server - migration runs automatically!
|
|
||||||
./start.sh
|
|
||||||
|
|
||||||
The migration will:
|
|
||||||
- Check if new columns exist
|
|
||||||
- Add them if missing (instructions, prep_time, image_url)
|
|
||||||
- Keep all your existing data
|
|
||||||
- No data loss
|
|
||||||
|
|
||||||
=== HOW TO VERIFY ===
|
|
||||||
|
|
||||||
After restart:
|
|
||||||
1. Go to Meals tab
|
|
||||||
2. Form should have:
|
|
||||||
- Name
|
- Name
|
||||||
- Description
|
- Description
|
||||||
- Type dropdown
|
- Type (breakfast/lunch/snack)
|
||||||
- Prep time (NEW)
|
- Prep time
|
||||||
- Image URL (NEW)
|
- Image URL
|
||||||
- Instructions textarea (NEW)
|
- Instructions
|
||||||
|
4. Change what you want
|
||||||
|
5. Click "Save" → Modal closes, meal updates
|
||||||
|
6. Click "Cancel" → Modal closes, no changes
|
||||||
|
|
||||||
If you see the new fields, it's working!
|
=== SECURITY ===
|
||||||
|
|
||||||
=== FEATURES ===
|
✅ User isolation enforced:
|
||||||
|
- GetMealByID(userID, mealID) verifies ownership
|
||||||
|
- Users CANNOT edit others' meals
|
||||||
|
- Users CANNOT access others' meal data
|
||||||
|
- UPDATE query filters by user_id
|
||||||
|
- All queries parameterized (SQL injection safe)
|
||||||
|
|
||||||
Instructions:
|
✅ Modal security:
|
||||||
- Multi-line textarea
|
- Closes on click outside
|
||||||
- Click to expand/collapse on meal card
|
- Close button works
|
||||||
- Optional
|
- No data exposed
|
||||||
|
- XSS protected (template escaping)
|
||||||
|
|
||||||
Prep Time:
|
=== UI FEATURES ===
|
||||||
- Number input (minutes)
|
|
||||||
- Shows as "⏱️ XX min" badge
|
|
||||||
- Optional
|
|
||||||
|
|
||||||
Image:
|
Modal Dialog:
|
||||||
- URL input
|
- Semi-transparent overlay
|
||||||
- Shows as 120x120px thumbnail
|
- Centered white box
|
||||||
- Optional
|
- All fields editable
|
||||||
|
- Save button (blue)
|
||||||
|
- Cancel button (gray)
|
||||||
|
- Click outside to close
|
||||||
|
- Clean, professional design
|
||||||
|
|
||||||
=== ALL FIELDS OPTIONAL ===
|
Buttons:
|
||||||
|
- Edit (orange) - opens modal
|
||||||
|
- Save (blue) - updates meal
|
||||||
|
- Cancel (gray) - closes modal
|
||||||
|
|
||||||
You can:
|
=== WHAT CAN BE EDITED ===
|
||||||
- Leave them blank
|
|
||||||
- Fill only some
|
|
||||||
- Fill all of them
|
|
||||||
|
|
||||||
Old meals without these fields work fine!
|
Everything:
|
||||||
|
- ✅ Name
|
||||||
|
- ✅ Description
|
||||||
|
- ✅ Meal type (breakfast/lunch/snack)
|
||||||
|
- ✅ Prep time
|
||||||
|
- ✅ Image URL
|
||||||
|
- ✅ Instructions
|
||||||
|
|
||||||
|
=== AFTER SAVE ===
|
||||||
|
|
||||||
|
- Modal closes automatically
|
||||||
|
- Meal card updates instantly
|
||||||
|
- No page reload (HTMX)
|
||||||
|
- All changes visible immediately
|
||||||
|
- Edit button still works
|
||||||
|
|
||||||
|
=== CODE CHANGES ===
|
||||||
|
|
||||||
|
handlers/meals.go:
|
||||||
|
- GetEditMealHandler() - shows modal with form
|
||||||
|
- UpdateMealHandler() - saves changes with security
|
||||||
|
- Added Edit button to meal cards
|
||||||
|
|
||||||
|
main.go:
|
||||||
|
- /meals/:id/edit route (GET)
|
||||||
|
- /meals/:id/update route (POST)
|
||||||
|
|
||||||
|
static/styles.css:
|
||||||
|
- Modal overlay styles
|
||||||
|
- Modal content styles
|
||||||
|
- Form styles
|
||||||
|
- Button styles
|
||||||
|
|
||||||
|
=== SECURITY CHECKS ===
|
||||||
|
|
||||||
|
Edit Modal:
|
||||||
|
1. Check session (middleware)
|
||||||
|
2. Get userID from context
|
||||||
|
3. Verify meal ownership
|
||||||
|
4. Show form if authorized
|
||||||
|
5. 404 if not found/unauthorized
|
||||||
|
|
||||||
|
Update:
|
||||||
|
1. Check session (middleware)
|
||||||
|
2. Get userID from context
|
||||||
|
3. Verify meal ownership BEFORE update
|
||||||
|
4. Validate all inputs
|
||||||
|
5. Validate meal type
|
||||||
|
6. UPDATE with user_id filter
|
||||||
|
7. Return 403 if unauthorized
|
||||||
|
|
||||||
|
SQL Queries:
|
||||||
|
- All parameterized
|
||||||
|
- No string concatenation
|
||||||
|
- User isolation enforced
|
||||||
|
- No SQL injection possible
|
||||||
|
|
||||||
=== READY TO USE ===
|
=== READY TO USE ===
|
||||||
|
|
||||||
✅ Migration included
|
✅ Build successful
|
||||||
✅ Auto-updates old databases
|
✅ Security implemented
|
||||||
✅ No data loss
|
✅ User isolation working
|
||||||
✅ All features work
|
✅ Modal working
|
||||||
|
✅ All features preserved
|
||||||
|
|
||||||
Just restart the server and you're good!
|
Just restart if needed!
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ func MealsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<button
|
||||||
|
hx-get="/meals/{{.ID}}/edit"
|
||||||
|
hx-target="#edit-modal"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="edit-btn">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
hx-get="/meals/{{.ID}}/ingredients"
|
hx-get="/meals/{{.ID}}/ingredients"
|
||||||
hx-target="#meal-{{.ID}}-ingredients"
|
hx-target="#meal-{{.ID}}-ingredients"
|
||||||
@@ -82,6 +89,9 @@ func MealsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div id="edit-modal"></div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -149,6 +159,13 @@ func AddMealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<button
|
||||||
|
hx-get="/meals/{{.ID}}/edit"
|
||||||
|
hx-target="#edit-modal"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="edit-btn">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
hx-get="/meals/{{.ID}}/ingredients"
|
hx-get="/meals/{{.ID}}/ingredients"
|
||||||
hx-target="#meal-{{.ID}}-ingredients"
|
hx-target="#meal-{{.ID}}-ingredients"
|
||||||
@@ -386,3 +403,214 @@ func DeleteMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEditMealHandler shows the edit modal for a meal
|
||||||
|
func GetEditMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := auth.GetUserID(r)
|
||||||
|
|
||||||
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mealID, err := strconv.Atoi(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid meal ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get meal with security check
|
||||||
|
meal, err := database.GetMealByID(userID, mealID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Meal not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := `
|
||||||
|
<div class="modal-overlay" id="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Edit Meal</h3>
|
||||||
|
<form hx-post="/meals/{{.ID}}/update"
|
||||||
|
hx-target="#meal-{{.ID}}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="edit-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name:</label>
|
||||||
|
<input type="text" name="name" value="{{.Name}}" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Description:</label>
|
||||||
|
<input type="text" name="description" value="{{.Description}}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Type:</label>
|
||||||
|
<select name="meal_type" required>
|
||||||
|
<option value="breakfast" {{if eq .MealType "breakfast"}}selected{{end}}>Breakfast</option>
|
||||||
|
<option value="lunch" {{if eq .MealType "lunch"}}selected{{end}}>Lunch</option>
|
||||||
|
<option value="snack" {{if eq .MealType "snack"}}selected{{end}}>Snack</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Prep Time (minutes):</label>
|
||||||
|
<input type="number" name="prep_time" value="{{.PrepTime}}" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Image URL:</label>
|
||||||
|
<input type="url" name="image_url" value="{{.ImageURL}}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Instructions:</label>
|
||||||
|
<textarea name="instructions" rows="5">{{.Instructions}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button type="submit" class="btn-primary">Save</button>
|
||||||
|
<button type="button" onclick="closeModal()" class="btn-secondary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('edit-modal').innerHTML = '';
|
||||||
|
}
|
||||||
|
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
||||||
|
if (e.target.id === 'modal-overlay') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
|
||||||
|
t := template.Must(template.New("editMeal").Parse(tmpl))
|
||||||
|
t.Execute(w, meal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMealHandler updates a meal
|
||||||
|
func UpdateMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := auth.GetUserID(r)
|
||||||
|
|
||||||
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mealID, err := strconv.Atoi(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid meal ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
_, err = database.GetMealByID(userID, mealID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Meal not found or access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
|
description := strings.TrimSpace(r.FormValue("description"))
|
||||||
|
mealType := r.FormValue("meal_type")
|
||||||
|
instructions := strings.TrimSpace(r.FormValue("instructions"))
|
||||||
|
imageURL := strings.TrimSpace(r.FormValue("image_url"))
|
||||||
|
|
||||||
|
prepTime := 0
|
||||||
|
if prepTimeStr := r.FormValue("prep_time"); prepTimeStr != "" {
|
||||||
|
prepTime, _ = strconv.Atoi(prepTimeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" || mealType == "" {
|
||||||
|
http.Error(w, "Name and meal type are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate meal type
|
||||||
|
if mealType != "breakfast" && mealType != "lunch" && mealType != "snack" {
|
||||||
|
http.Error(w, "Invalid meal type", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update meal
|
||||||
|
_, err = database.DB.Exec(
|
||||||
|
"UPDATE meals SET name = ?, description = ?, meal_type = ?, instructions = ?, prep_time = ?, image_url = ? WHERE id = ? AND user_id = ?",
|
||||||
|
name, description, mealType, instructions, prepTime, imageURL, mealID, userID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated meal card
|
||||||
|
tmpl := `
|
||||||
|
<div class="meal-item" id="meal-{{.ID}}">
|
||||||
|
<div class="meal-header">
|
||||||
|
{{if .ImageURL}}
|
||||||
|
<img src="{{.ImageURL}}" alt="{{.Name}}" class="meal-image" />
|
||||||
|
{{end}}
|
||||||
|
<div class="meal-info-section">
|
||||||
|
<div>
|
||||||
|
<span class="item-name">{{.Name}}</span>
|
||||||
|
<span class="meal-type-tag tag-{{.MealType}}">{{.MealType}}</span>
|
||||||
|
{{if gt .PrepTime 0}}
|
||||||
|
<span class="prep-time">⏱️ {{.PrepTime}} min</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<span class="item-description">{{.Description}}</span>
|
||||||
|
{{if .Instructions}}
|
||||||
|
<details class="instructions-preview">
|
||||||
|
<summary>Instructions</summary>
|
||||||
|
<p class="instructions-text">{{.Instructions}}</p>
|
||||||
|
</details>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
hx-get="/meals/{{.ID}}/edit"
|
||||||
|
hx-target="#edit-modal"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="edit-btn">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
hx-get="/meals/{{.ID}}/ingredients"
|
||||||
|
hx-target="#meal-{{.ID}}-ingredients"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="view-btn">
|
||||||
|
View Ingredients
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
hx-delete="/meals/{{.ID}}"
|
||||||
|
hx-target="#meal-{{.ID}}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-confirm="Are you sure you want to delete this meal?"
|
||||||
|
class="delete-btn">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="meal-{{.ID}}-ingredients" class="meal-ingredients"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.getElementById('edit-modal').innerHTML = '';
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
MealType string
|
||||||
|
Instructions string
|
||||||
|
PrepTime int
|
||||||
|
ImageURL string
|
||||||
|
}{mealID, name, description, mealType, instructions, prepTime, imageURL}
|
||||||
|
|
||||||
|
t := template.Must(template.New("updatedMeal").Parse(tmpl))
|
||||||
|
t.Execute(w, data)
|
||||||
|
}
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -94,6 +94,20 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
|
} else if strings.Contains(path, "/edit") {
|
||||||
|
// Meal edit route
|
||||||
|
if r.Method == "GET" {
|
||||||
|
handlers.GetEditMealHandler(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
} else if strings.Contains(path, "/update") {
|
||||||
|
// Meal update route
|
||||||
|
if r.Method == "POST" {
|
||||||
|
handlers.UpdateMealHandler(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Meal delete route
|
// Meal delete route
|
||||||
if r.Method == "DELETE" {
|
if r.Method == "DELETE" {
|
||||||
|
|||||||
@@ -587,6 +587,125 @@ button:hover {
|
|||||||
background-color: rgba(255, 255, 255, 0.3);
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form .form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form .form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form .form-group input,
|
||||||
|
.edit-form .form-group select,
|
||||||
|
.edit-form .form-group textarea {
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form .form-group input:focus,
|
||||||
|
.edit-form .form-group select:focus,
|
||||||
|
.edit-form .form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form .form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
background-color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
background-color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
Reference in New Issue
Block a user