diff --git a/samples/web-app-cosmosdb-mongodb-api/python/images/vacation-planner.png b/samples/web-app-cosmosdb-mongodb-api/python/images/vacation-planner.png index 7fbb14f..a7c5151 100644 Binary files a/samples/web-app-cosmosdb-mongodb-api/python/images/vacation-planner.png and b/samples/web-app-cosmosdb-mongodb-api/python/images/vacation-planner.png differ diff --git a/samples/web-app-cosmosdb-mongodb-api/python/src/.zip b/samples/web-app-cosmosdb-mongodb-api/python/src/.zip deleted file mode 100644 index ba51ef8..0000000 Binary files a/samples/web-app-cosmosdb-mongodb-api/python/src/.zip and /dev/null differ diff --git a/samples/web-app-cosmosdb-mongodb-api/python/src/app.py b/samples/web-app-cosmosdb-mongodb-api/python/src/app.py index 0bac228..aab8e20 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/src/app.py +++ b/samples/web-app-cosmosdb-mongodb-api/python/src/app.py @@ -3,12 +3,13 @@ import datetime import logging from typing import List, Tuple -from flask import Flask, render_template, request, redirect, url_for +from flask import Flask, flash, render_template, request, redirect, url_for from mongodb import MongoDbClient import hashlib # Initialize Flask application app: Flask = Flask(__name__) +app.secret_key = os.environ.get("SECRET_KEY", os.urandom(24)) # Configure logging logging.basicConfig( @@ -68,10 +69,6 @@ def read_documents(username: str | None = None) -> List[dict]: @app.route('/', methods=['GET', 'POST']) def index(): """Handle the main page for viewing and adding activities.""" - # Get edit data from query parameters (if any) - edit_id = request.args.get('edit_id') - edit_activity = request.args.get('edit_activity') - if request.method == 'POST': activity = request.form.get('activity') if activity: @@ -85,6 +82,7 @@ def index(): updated_activity = mongodb_client.update_document_by_id(row_id, {"activity": activity}) if updated_activity: logger.info(f"Activity updated: {row_id}") + flash("Activity updated!") else: # Create a document with the activity provided document: dict = create_document(activity) @@ -93,6 +91,7 @@ def index(): # Append the activity to the activities list activities.append((document["_id"], activity)) logger.info(f"Activity added: {activity}") + flash("Activity added!") except (ConnectionError, ValueError) as e: logger.error("Error creating document: %s", e) @@ -102,7 +101,7 @@ def index(): # Always reload activities from Cosmos DB on GET (refresh) read_documents(username) - return render_template('index.html', activities=activities, username=username, edit_id=edit_id, edit_activity=edit_activity) + return render_template('index.html', activities=activities, username=username) @app.route('/favicon.ico') def favicon(): @@ -115,20 +114,7 @@ def delete(activity_id: int): if 0 <= activity_id < len(activities): # Delete the document from MongoDB mongodb_client.delete_document_by_id(activities[activity_id][0]) - - return redirect(url_for('index')) - -@app.route('/update/', methods=['GET']) -def update(activity_id: int): - """Handle updating of an activity by its index in the list.""" - try: - if 0 <= activity_id < len(activities): - db_activity_id = activities[activity_id][0] - activity_text = activities[activity_id][1] - # Redirect to index with edit parameters - return redirect(url_for('index', edit_id=db_activity_id, edit_activity=activity_text)) - except (ConnectionError, ValueError) as e: - logger.error("Error preparing activity for update: %s", e) + flash("Activity deleted.") return redirect(url_for('index')) diff --git a/samples/web-app-cosmosdb-mongodb-api/python/src/static/favicon.ico:Zone.Identifier b/samples/web-app-cosmosdb-mongodb-api/python/src/static/favicon.ico:Zone.Identifier deleted file mode 100644 index 603e0df..0000000 --- a/samples/web-app-cosmosdb-mongodb-api/python/src/static/favicon.ico:Zone.Identifier +++ /dev/null @@ -1,4 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=https://portal.azure.com/Content/favicon.ico -HostUrl=https://portal.azure.com/Content/favicon.ico diff --git a/samples/web-app-cosmosdb-mongodb-api/python/src/static/style.css b/samples/web-app-cosmosdb-mongodb-api/python/src/static/style.css index 8fb702c..67508fa 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/src/static/style.css +++ b/samples/web-app-cosmosdb-mongodb-api/python/src/static/style.css @@ -1,98 +1,341 @@ -.action-col { - padding-left: 0 !important; - padding-right: 0 !important; +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --teal-50: #f0fafb; + --teal-100: #d0f0f5; + --teal-500: #0e9db0; + --teal-600: #0e6ba8; + --teal-700: #0a5a8e; + --teal-800: #074d78; + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-700: #374151; + --gray-900: #111827; + --white: #ffffff; + --bg: #f0f8ff; + --shadow-sm: 0 1px 2px rgba(0,0,0,.06); + --shadow: 0 4px 6px -1px rgba(0,0,0,.10), 0 2px 4px -2px rgba(0,0,0,.06); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.10), 0 4px 6px -4px rgba(0,0,0,.06); + --radius: 12px; + --toast-bg: #111827; + --toast-fg: #ffffff; } -.action-col .sea-btn { - margin-left: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; + +html[data-theme="dark"] { + --gray-50: #0f172a; + --gray-100: #1e293b; + --gray-200: #334155; + --gray-400: #94a3b8; + --gray-500: #cbd5e1; + --gray-700: #e2e8f0; + --gray-900: #f8fafc; + --white: #1e293b; + --bg: #0a1929; + --teal-50: #0e2a38; + --teal-700: #7dd3e8; + --shadow-sm: 0 1px 2px rgba(0,0,0,.4); + --shadow: 0 4px 6px -1px rgba(0,0,0,.5), 0 2px 4px -2px rgba(0,0,0,.4); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.6), 0 4px 6px -4px rgba(0,0,0,.4); + --toast-bg: #334155; + --toast-fg: #f8fafc; } + body { - background: linear-gradient(135deg, #fff 0%, #a2d4f7 100%); - font-family: 'Segoe UI', sans-serif; - margin: 0; - padding: 0; - min-height: 100vh; -} - -.sea-title { - color: #0e6ba8; -} -.sea-form input[type="text"] { - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - color: #0e6ba8; -} -.sea-form input[type="text"]:focus { - border: 2px solid #0e6ba8; - box-shadow: none; - outline: none; -} -.sea-btn { - background: #fff; - color: #0e6ba8; - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - font-weight: 600; - transition: background 0.2s, color 0.2s; - width: 120px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - margin-left: auto; - padding: 0; -} -.sea-btn:hover { - background: #0e6ba8; - color: #fff; -} -.sea-form { - display: flex; - gap: 0; - justify-content: flex-end; - margin-bottom: 24px; -} -.sea-form .form-control { - margin-right: 10px; -} -.sea-table th, .sea-table td { - border-bottom: 2px solid #0e6ba8 !important; - color: #0e6ba8; -} -.sea-table th { - background: #e3f6fd; -} - -/* Remove left padding for Action column to align header and button */ -.action-col { - padding-left: 0 !important; -} -.sea-table .btn-danger { - border: 2px solid #0e6ba8; - background: #fff; - color: #0e6ba8; -} -.sea-table .btn-danger:hover { - background: #0e6ba8; - color: #fff; -} -.sea-table .text-muted { - color: #7bb7e0 !important; -} -.container { - max-width: 600px; - margin: 40px auto; - background: rgba(255,255,255,0.95); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0,0,0,0.15); - padding: 32px 24px 24px 24px; - text-align: center; -} -.banner { - width: 100%; - border-radius: 12px; - margin-bottom: 16px; + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg); + color: var(--gray-900); + min-height: 100vh; + transition: background 0.2s, color 0.2s; +} + +/* ── Header ─────────────────────────────────────────── */ +header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.5rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + box-shadow: var(--shadow-lg); +} + +.header-left h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; } +.header-left p { font-size: 0.85rem; opacity: 0.8; margin-top: 2px; } + +.header-right { display: flex; align-items: center; gap: 0.6rem; } + +#btn-dark-mode { + background: rgba(255,255,255,.15); + color: #ffffff; + border: 1.5px solid rgba(255,255,255,.3); + border-radius: 8px; + padding: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, transform 0.1s; +} +#btn-dark-mode:hover { background: rgba(255,255,255,.25); transform: translateY(-1px); } + +#btn-add { + background: #ffffff; + color: var(--teal-700); + border: none; + border-radius: 8px; + padding: 0.55rem 1.2rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.4rem; + transition: background 0.15s, transform 0.1s; + white-space: nowrap; +} +#btn-add:hover { background: var(--teal-50); transform: translateY(-1px); } + +/* ── Content area ────────────────────────────────────── */ +.content { + max-width: 820px; + margin: 2rem auto; + padding: 0 1.5rem 3rem; +} + +/* ── Table ───────────────────────────────────────────── */ +#activity-table { + width: 100%; + border-collapse: collapse; + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; + transition: background 0.2s; +} + +#activity-table thead tr { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; +} + +#activity-table th { + padding: 0.85rem 1.1rem; + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.02em; + text-align: left; +} + +#activity-table th.col-actions { text-align: center; } + +#activity-table td { + padding: 0.75rem 1.1rem; + font-size: 0.93rem; + color: var(--gray-900); + border-bottom: 1px solid var(--gray-200); + transition: background 0.15s, color 0.2s, border-color 0.2s; +} + +#activity-table tbody tr:last-child td { border-bottom: none; } +#activity-table tbody tr:hover td { background: var(--teal-50); } + +.col-btn { + width: 1px; + text-align: center; + padding-left: 0.3rem !important; + padding-right: 0.3rem !important; + white-space: nowrap; +} + +#activity-table td.col-btn:last-child { padding-right: 0.6rem !important; } + +/* ── Row action buttons ──────────────────────────────── */ +.btn-edit, .btn-delete { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.55rem 0.75rem; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s, transform 0.1s; + white-space: nowrap; + width: 90px; + justify-content: center; +} + +.btn-edit { + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); +} + +.btn-edit:hover { + background: var(--teal-50); + transform: translateY(-1px); +} + +.btn-delete { + border: none; + background: var(--teal-600); + color: #ffffff; +} + +.btn-delete:hover { + background: var(--teal-700); + transform: translateY(-1px); +} + +/* ── Empty cell ──────────────────────────────────────── */ +.empty-cell { + text-align: center; + color: var(--gray-400) !important; + font-style: italic; + padding: 3rem 1rem !important; +} + +/* ── Modal overlay ───────────────────────────────────── */ +#overlay, #delete-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,.45); + z-index: 100; + align-items: center; + justify-content: center; + padding: 1rem; +} +#overlay.open, #delete-overlay.open { display: flex; } + +.modal { + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 460px; + overflow: hidden; + transition: background 0.2s; +} + +.modal-header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.1rem 1.4rem; + display: flex; + align-items: center; + justify-content: space-between; +} +.modal-header h2 { font-size: 1rem; font-weight: 600; } + +#btn-close-modal, #btn-close-delete-modal { + background: none; + border: none; + color: rgba(255,255,255,.8); + cursor: pointer; + font-size: 1.4rem; + line-height: 1; + padding: 2px; + transition: color 0.15s; +} +#btn-close-modal:hover, #btn-close-delete-modal:hover { color: #ffffff; } + +.modal-body { + padding: 1.4rem; + color: var(--gray-700); + font-size: 0.93rem; + line-height: 1.5; + transition: color 0.2s; +} + +.modal form { + padding: 1.4rem; + display: flex; + flex-direction: column; + gap: 1rem; } + +.field { display: flex; flex-direction: column; gap: 0.3rem; } + +.field label { font-size: 0.82rem; font-weight: 600; color: var(--gray-700); } + +.field input { + padding: 0.55rem 0.8rem; + border: 1.5px solid var(--gray-200); + border-radius: 7px; + font-size: 0.9rem; + font-family: inherit; + color: var(--gray-900); + background: var(--white); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s, background 0.2s, color 0.2s; +} +.field input:focus { + border-color: var(--teal-500); + box-shadow: 0 0 0 3px rgba(14,109,168,.15); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.6rem; + padding: 0 1.4rem 1.4rem; +} + +.btn-secondary { + padding: 0.55rem 1.1rem; + border-radius: 7px; + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s; +} +.btn-secondary:hover { background: var(--teal-50); } + +.btn-primary { + padding: 0.55rem 1.3rem; + border-radius: 7px; + border: none; + background: var(--teal-600); + color: #ffffff; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} +.btn-primary:hover { background: var(--teal-700); } + +/* ── Toast ───────────────────────────────────────────── */ +#toast { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + background: var(--toast-bg); + color: var(--toast-fg); + padding: 0.65rem 1.1rem; + border-radius: 8px; + font-size: 0.85rem; + opacity: 0; + transform: translateY(8px); + pointer-events: none; + transition: opacity 0.2s, transform 0.2s; + z-index: 200; +} +#toast.show { opacity: 1; transform: none; } + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 600px) { + header { padding: 1.2rem 1rem; } + .content { padding: 1rem 0.75rem 3rem; } + .col-btn { white-space: nowrap; } + .btn-edit, .btn-delete { width: auto; padding: 0.55rem 0.5rem; } +} + diff --git a/samples/web-app-cosmosdb-mongodb-api/python/src/static/summer_banner.jpg b/samples/web-app-cosmosdb-mongodb-api/python/src/static/summer_banner.jpg deleted file mode 100644 index 2641b9d..0000000 Binary files a/samples/web-app-cosmosdb-mongodb-api/python/src/static/summer_banner.jpg and /dev/null differ diff --git a/samples/web-app-cosmosdb-mongodb-api/python/src/templates/index.html b/samples/web-app-cosmosdb-mongodb-api/python/src/templates/index.html index d9e0cfa..45a7be8 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/src/templates/index.html +++ b/samples/web-app-cosmosdb-mongodb-api/python/src/templates/index.html @@ -1,69 +1,260 @@ - - Vacation Planner - - - + + + Vacation Planner + + + + + - -
- Summer Banner -

Vacation Planner

-
-
- - - -
-
-
- - - - - - - - - {% for activity in activities %} - - - - - - {% else %} - - - - {% endfor %} - -
ActivityAction
{{ activity[1] }} - Edit - -
- - -
-
No vacation plans yet!
-
+ + + +
+
+

🌴 Vacation Planner

+

{{ activities|length }} activit{{ 'ies' if activities|length != 1 else 'y' }} planned

+
+
+ + +
+
+ + +
+ + + + + + + + + {% for activity in activities %} + + + + + + {% else %} + + + + {% endfor %} + +
ActivityActions
{{ activity[1] }} + + +
+ +
+
No vacation plans yet — add your first activity!
+
+ + +
+ +
+ + +
+ +
+ + +
+ + diff --git a/samples/web-app-cosmosdb-nosql-api/python/images/vacation-planner.png b/samples/web-app-cosmosdb-nosql-api/python/images/vacation-planner.png index 7087545..a7c5151 100644 Binary files a/samples/web-app-cosmosdb-nosql-api/python/images/vacation-planner.png and b/samples/web-app-cosmosdb-nosql-api/python/images/vacation-planner.png differ diff --git a/samples/web-app-cosmosdb-nosql-api/python/src/app.py b/samples/web-app-cosmosdb-nosql-api/python/src/app.py index 3ae7cd4..01ec002 100644 --- a/samples/web-app-cosmosdb-nosql-api/python/src/app.py +++ b/samples/web-app-cosmosdb-nosql-api/python/src/app.py @@ -2,11 +2,12 @@ import datetime import logging import hashlib -from flask import Flask, render_template, request, redirect, url_for +from flask import Flask, flash, render_template, request, redirect, url_for from cosmosdb_client import CosmosDbClient app = Flask(__name__) +app.secret_key = os.environ.get('FLASK_SECRET_KEY', os.urandom(24)) app.debug = True logging.basicConfig( @@ -84,10 +85,12 @@ def index(): username=username, updates={"activity": activity} ) + flash('Activity updated.') else: doc = create_document(activity) get_cosmos().insert_document(doc) get_activities().add((doc["id"], activity)) + flash('Activity added.') return redirect(url_for('index')) @@ -107,6 +110,7 @@ def delete(activity_id: str): # Direct deletion using the ID passed in the URL get_cosmos().delete_document_by_id(activity_id, username) + flash('Activity deleted.') return redirect(url_for('index')) diff --git a/samples/web-app-cosmosdb-nosql-api/python/src/static/style.css b/samples/web-app-cosmosdb-nosql-api/python/src/static/style.css index 8fb702c..67508fa 100644 --- a/samples/web-app-cosmosdb-nosql-api/python/src/static/style.css +++ b/samples/web-app-cosmosdb-nosql-api/python/src/static/style.css @@ -1,98 +1,341 @@ -.action-col { - padding-left: 0 !important; - padding-right: 0 !important; +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --teal-50: #f0fafb; + --teal-100: #d0f0f5; + --teal-500: #0e9db0; + --teal-600: #0e6ba8; + --teal-700: #0a5a8e; + --teal-800: #074d78; + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-700: #374151; + --gray-900: #111827; + --white: #ffffff; + --bg: #f0f8ff; + --shadow-sm: 0 1px 2px rgba(0,0,0,.06); + --shadow: 0 4px 6px -1px rgba(0,0,0,.10), 0 2px 4px -2px rgba(0,0,0,.06); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.10), 0 4px 6px -4px rgba(0,0,0,.06); + --radius: 12px; + --toast-bg: #111827; + --toast-fg: #ffffff; } -.action-col .sea-btn { - margin-left: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; + +html[data-theme="dark"] { + --gray-50: #0f172a; + --gray-100: #1e293b; + --gray-200: #334155; + --gray-400: #94a3b8; + --gray-500: #cbd5e1; + --gray-700: #e2e8f0; + --gray-900: #f8fafc; + --white: #1e293b; + --bg: #0a1929; + --teal-50: #0e2a38; + --teal-700: #7dd3e8; + --shadow-sm: 0 1px 2px rgba(0,0,0,.4); + --shadow: 0 4px 6px -1px rgba(0,0,0,.5), 0 2px 4px -2px rgba(0,0,0,.4); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.6), 0 4px 6px -4px rgba(0,0,0,.4); + --toast-bg: #334155; + --toast-fg: #f8fafc; } + body { - background: linear-gradient(135deg, #fff 0%, #a2d4f7 100%); - font-family: 'Segoe UI', sans-serif; - margin: 0; - padding: 0; - min-height: 100vh; -} - -.sea-title { - color: #0e6ba8; -} -.sea-form input[type="text"] { - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - color: #0e6ba8; -} -.sea-form input[type="text"]:focus { - border: 2px solid #0e6ba8; - box-shadow: none; - outline: none; -} -.sea-btn { - background: #fff; - color: #0e6ba8; - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - font-weight: 600; - transition: background 0.2s, color 0.2s; - width: 120px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - margin-left: auto; - padding: 0; -} -.sea-btn:hover { - background: #0e6ba8; - color: #fff; -} -.sea-form { - display: flex; - gap: 0; - justify-content: flex-end; - margin-bottom: 24px; -} -.sea-form .form-control { - margin-right: 10px; -} -.sea-table th, .sea-table td { - border-bottom: 2px solid #0e6ba8 !important; - color: #0e6ba8; -} -.sea-table th { - background: #e3f6fd; -} - -/* Remove left padding for Action column to align header and button */ -.action-col { - padding-left: 0 !important; -} -.sea-table .btn-danger { - border: 2px solid #0e6ba8; - background: #fff; - color: #0e6ba8; -} -.sea-table .btn-danger:hover { - background: #0e6ba8; - color: #fff; -} -.sea-table .text-muted { - color: #7bb7e0 !important; -} -.container { - max-width: 600px; - margin: 40px auto; - background: rgba(255,255,255,0.95); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0,0,0,0.15); - padding: 32px 24px 24px 24px; - text-align: center; -} -.banner { - width: 100%; - border-radius: 12px; - margin-bottom: 16px; + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg); + color: var(--gray-900); + min-height: 100vh; + transition: background 0.2s, color 0.2s; +} + +/* ── Header ─────────────────────────────────────────── */ +header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.5rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + box-shadow: var(--shadow-lg); +} + +.header-left h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; } +.header-left p { font-size: 0.85rem; opacity: 0.8; margin-top: 2px; } + +.header-right { display: flex; align-items: center; gap: 0.6rem; } + +#btn-dark-mode { + background: rgba(255,255,255,.15); + color: #ffffff; + border: 1.5px solid rgba(255,255,255,.3); + border-radius: 8px; + padding: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, transform 0.1s; +} +#btn-dark-mode:hover { background: rgba(255,255,255,.25); transform: translateY(-1px); } + +#btn-add { + background: #ffffff; + color: var(--teal-700); + border: none; + border-radius: 8px; + padding: 0.55rem 1.2rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.4rem; + transition: background 0.15s, transform 0.1s; + white-space: nowrap; +} +#btn-add:hover { background: var(--teal-50); transform: translateY(-1px); } + +/* ── Content area ────────────────────────────────────── */ +.content { + max-width: 820px; + margin: 2rem auto; + padding: 0 1.5rem 3rem; +} + +/* ── Table ───────────────────────────────────────────── */ +#activity-table { + width: 100%; + border-collapse: collapse; + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; + transition: background 0.2s; +} + +#activity-table thead tr { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; +} + +#activity-table th { + padding: 0.85rem 1.1rem; + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.02em; + text-align: left; +} + +#activity-table th.col-actions { text-align: center; } + +#activity-table td { + padding: 0.75rem 1.1rem; + font-size: 0.93rem; + color: var(--gray-900); + border-bottom: 1px solid var(--gray-200); + transition: background 0.15s, color 0.2s, border-color 0.2s; +} + +#activity-table tbody tr:last-child td { border-bottom: none; } +#activity-table tbody tr:hover td { background: var(--teal-50); } + +.col-btn { + width: 1px; + text-align: center; + padding-left: 0.3rem !important; + padding-right: 0.3rem !important; + white-space: nowrap; +} + +#activity-table td.col-btn:last-child { padding-right: 0.6rem !important; } + +/* ── Row action buttons ──────────────────────────────── */ +.btn-edit, .btn-delete { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.55rem 0.75rem; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s, transform 0.1s; + white-space: nowrap; + width: 90px; + justify-content: center; +} + +.btn-edit { + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); +} + +.btn-edit:hover { + background: var(--teal-50); + transform: translateY(-1px); +} + +.btn-delete { + border: none; + background: var(--teal-600); + color: #ffffff; +} + +.btn-delete:hover { + background: var(--teal-700); + transform: translateY(-1px); +} + +/* ── Empty cell ──────────────────────────────────────── */ +.empty-cell { + text-align: center; + color: var(--gray-400) !important; + font-style: italic; + padding: 3rem 1rem !important; +} + +/* ── Modal overlay ───────────────────────────────────── */ +#overlay, #delete-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,.45); + z-index: 100; + align-items: center; + justify-content: center; + padding: 1rem; +} +#overlay.open, #delete-overlay.open { display: flex; } + +.modal { + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 460px; + overflow: hidden; + transition: background 0.2s; +} + +.modal-header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.1rem 1.4rem; + display: flex; + align-items: center; + justify-content: space-between; +} +.modal-header h2 { font-size: 1rem; font-weight: 600; } + +#btn-close-modal, #btn-close-delete-modal { + background: none; + border: none; + color: rgba(255,255,255,.8); + cursor: pointer; + font-size: 1.4rem; + line-height: 1; + padding: 2px; + transition: color 0.15s; +} +#btn-close-modal:hover, #btn-close-delete-modal:hover { color: #ffffff; } + +.modal-body { + padding: 1.4rem; + color: var(--gray-700); + font-size: 0.93rem; + line-height: 1.5; + transition: color 0.2s; +} + +.modal form { + padding: 1.4rem; + display: flex; + flex-direction: column; + gap: 1rem; } + +.field { display: flex; flex-direction: column; gap: 0.3rem; } + +.field label { font-size: 0.82rem; font-weight: 600; color: var(--gray-700); } + +.field input { + padding: 0.55rem 0.8rem; + border: 1.5px solid var(--gray-200); + border-radius: 7px; + font-size: 0.9rem; + font-family: inherit; + color: var(--gray-900); + background: var(--white); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s, background 0.2s, color 0.2s; +} +.field input:focus { + border-color: var(--teal-500); + box-shadow: 0 0 0 3px rgba(14,109,168,.15); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.6rem; + padding: 0 1.4rem 1.4rem; +} + +.btn-secondary { + padding: 0.55rem 1.1rem; + border-radius: 7px; + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s; +} +.btn-secondary:hover { background: var(--teal-50); } + +.btn-primary { + padding: 0.55rem 1.3rem; + border-radius: 7px; + border: none; + background: var(--teal-600); + color: #ffffff; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} +.btn-primary:hover { background: var(--teal-700); } + +/* ── Toast ───────────────────────────────────────────── */ +#toast { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + background: var(--toast-bg); + color: var(--toast-fg); + padding: 0.65rem 1.1rem; + border-radius: 8px; + font-size: 0.85rem; + opacity: 0; + transform: translateY(8px); + pointer-events: none; + transition: opacity 0.2s, transform 0.2s; + z-index: 200; +} +#toast.show { opacity: 1; transform: none; } + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 600px) { + header { padding: 1.2rem 1rem; } + .content { padding: 1rem 0.75rem 3rem; } + .col-btn { white-space: nowrap; } + .btn-edit, .btn-delete { width: auto; padding: 0.55rem 0.5rem; } +} + diff --git a/samples/web-app-cosmosdb-nosql-api/python/src/static/summer_banner.jpg b/samples/web-app-cosmosdb-nosql-api/python/src/static/summer_banner.jpg deleted file mode 100644 index 2641b9d..0000000 Binary files a/samples/web-app-cosmosdb-nosql-api/python/src/static/summer_banner.jpg and /dev/null differ diff --git a/samples/web-app-cosmosdb-nosql-api/python/src/templates/index.html b/samples/web-app-cosmosdb-nosql-api/python/src/templates/index.html index 0b55e9c..efb642d 100644 --- a/samples/web-app-cosmosdb-nosql-api/python/src/templates/index.html +++ b/samples/web-app-cosmosdb-nosql-api/python/src/templates/index.html @@ -1,69 +1,260 @@ - - Vacation Planner - - - + + + Vacation Planner + + + + + - -
- Summer Banner - -

Vacation Planner

- -
-
- - - -
-
- -
- - - - - - - - - {% for activity in activities %} - - - - - - {% else %} - - - - {% endfor %} - -
ActivityAction
{{ activity[1] }} -
- -
-
No vacation plans yet!
-
+ + + +
+
+

🌴 Vacation Planner

+

{{ activities|length }} activit{{ 'ies' if activities|length != 1 else 'y' }} planned

+
+
+ + +
+
+ + +
+ + + + + + + + + {% for activity in activities %} + + + + + + {% else %} + + + + {% endfor %} + +
ActivityActions
{{ activity[1] }} + + +
+ +
+
No vacation plans yet — add your first activity!
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + // ── Escape closes any open modal ───────────────────────── + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { closeActivityModal(); closeDeleteModal(); } + }); + - \ No newline at end of file + diff --git a/samples/web-app-managed-identity/python/images/vacation-planner.png b/samples/web-app-managed-identity/python/images/vacation-planner.png index a8a47f8..a7c5151 100644 Binary files a/samples/web-app-managed-identity/python/images/vacation-planner.png and b/samples/web-app-managed-identity/python/images/vacation-planner.png differ diff --git a/samples/web-app-managed-identity/python/src/app.py b/samples/web-app-managed-identity/python/src/app.py index 30a7eb8..de56959 100644 --- a/samples/web-app-managed-identity/python/src/app.py +++ b/samples/web-app-managed-identity/python/src/app.py @@ -5,10 +5,11 @@ from azure.identity import DefaultAzureCredential, ClientSecretCredential from azure.storage.blob import BlobServiceClient from azure.core.exceptions import ResourceExistsError -from flask import Flask, render_template, request, redirect, url_for +from flask import Flask, flash, render_template, request, redirect, url_for # Initialize Flask application app: Flask = Flask(__name__) +app.secret_key = os.environ.get("SECRET_KEY", os.urandom(24)) client_id: str | None client_secret: str | None @@ -197,28 +198,51 @@ def delete_blob(name: str): except Exception as ex: print(f"An error occurred while deleting the blob: {ex}") +def update_blob(name: str, content: str): + """Update the content of an existing blob (overwrite in place).""" + global blob_service_client, container_name + if not name or not content: + raise ValueError("Both 'name' and 'content' must be provided.") + try: + if not blob_service_client: + raise ValueError("BlobServiceClient is not initialized.") + if not container_name: + raise ValueError("Container name is not set.") + container_client = blob_service_client.get_container_client(container_name) + blob_client = container_client.get_blob_client(name) + print(f"Updating blob '{name}' in container '{container_name}'.") + with io.BytesIO(content.encode("utf-8")) as content_stream: + blob_client.upload_blob(content_stream, blob_type="BlockBlob", overwrite=True) + print(f"Blob '{name}' updated successfully.") + except ValueError as ve: + print(f"Configuration Error: {ve}") + except Exception as ex: + print(f"An error occurred while updating the blob: {ex}") + @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': - activity = request.form.get('activity') + row_id = request.form.get('row_id', '').strip() + activity = request.form.get('activity', '').strip() if activity: - # Generate a unique blob name with a timestamp - timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") - name = f"{timestamp}-activity.txt" - - try: - # Create a blob with the name provided + if row_id: + # Update existing blob content in place + update_blob(row_id, activity) + for i, act in enumerate(activities): + if act[0] == row_id: + activities[i] = (row_id, activity) + break + flash('Activity updated successfully.') + else: + # Generate a unique blob name with a timestamp + timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + name = f"{timestamp}-activity.txt" create_blob_if_not_exists(name, activity) - - # Append the activity to the activities list activities.append((name, activity)) - - except Exception as e: - print(f"Error creating blob: {e}") - + flash('Activity added successfully.') return redirect(url_for('index')) - - # Always reload activities from blob storage on GET (refresh) + + # Always reload activities from blob storage on GET activities.clear() read_blobs_from_container() return render_template('index.html', activities=activities) @@ -226,11 +250,9 @@ def index(): @app.route('/delete/', methods=['POST']) def delete(activity_id): if 0 <= activity_id < len(activities): - # Delete the blob associated with the activity delete_blob(activities[activity_id][0]) - - # Remove the activity from the list activities.pop(activity_id) + flash('Activity deleted successfully.') return redirect(url_for('index')) # Initialize the application and Azure services when the module is loaded. diff --git a/samples/web-app-managed-identity/python/src/static/style.css b/samples/web-app-managed-identity/python/src/static/style.css index 8fb702c..67508fa 100644 --- a/samples/web-app-managed-identity/python/src/static/style.css +++ b/samples/web-app-managed-identity/python/src/static/style.css @@ -1,98 +1,341 @@ -.action-col { - padding-left: 0 !important; - padding-right: 0 !important; +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --teal-50: #f0fafb; + --teal-100: #d0f0f5; + --teal-500: #0e9db0; + --teal-600: #0e6ba8; + --teal-700: #0a5a8e; + --teal-800: #074d78; + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-700: #374151; + --gray-900: #111827; + --white: #ffffff; + --bg: #f0f8ff; + --shadow-sm: 0 1px 2px rgba(0,0,0,.06); + --shadow: 0 4px 6px -1px rgba(0,0,0,.10), 0 2px 4px -2px rgba(0,0,0,.06); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.10), 0 4px 6px -4px rgba(0,0,0,.06); + --radius: 12px; + --toast-bg: #111827; + --toast-fg: #ffffff; } -.action-col .sea-btn { - margin-left: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; + +html[data-theme="dark"] { + --gray-50: #0f172a; + --gray-100: #1e293b; + --gray-200: #334155; + --gray-400: #94a3b8; + --gray-500: #cbd5e1; + --gray-700: #e2e8f0; + --gray-900: #f8fafc; + --white: #1e293b; + --bg: #0a1929; + --teal-50: #0e2a38; + --teal-700: #7dd3e8; + --shadow-sm: 0 1px 2px rgba(0,0,0,.4); + --shadow: 0 4px 6px -1px rgba(0,0,0,.5), 0 2px 4px -2px rgba(0,0,0,.4); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.6), 0 4px 6px -4px rgba(0,0,0,.4); + --toast-bg: #334155; + --toast-fg: #f8fafc; } + body { - background: linear-gradient(135deg, #fff 0%, #a2d4f7 100%); - font-family: 'Segoe UI', sans-serif; - margin: 0; - padding: 0; - min-height: 100vh; -} - -.sea-title { - color: #0e6ba8; -} -.sea-form input[type="text"] { - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - color: #0e6ba8; -} -.sea-form input[type="text"]:focus { - border: 2px solid #0e6ba8; - box-shadow: none; - outline: none; -} -.sea-btn { - background: #fff; - color: #0e6ba8; - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - font-weight: 600; - transition: background 0.2s, color 0.2s; - width: 120px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - margin-left: auto; - padding: 0; -} -.sea-btn:hover { - background: #0e6ba8; - color: #fff; -} -.sea-form { - display: flex; - gap: 0; - justify-content: flex-end; - margin-bottom: 24px; -} -.sea-form .form-control { - margin-right: 10px; -} -.sea-table th, .sea-table td { - border-bottom: 2px solid #0e6ba8 !important; - color: #0e6ba8; -} -.sea-table th { - background: #e3f6fd; -} - -/* Remove left padding for Action column to align header and button */ -.action-col { - padding-left: 0 !important; -} -.sea-table .btn-danger { - border: 2px solid #0e6ba8; - background: #fff; - color: #0e6ba8; -} -.sea-table .btn-danger:hover { - background: #0e6ba8; - color: #fff; -} -.sea-table .text-muted { - color: #7bb7e0 !important; -} -.container { - max-width: 600px; - margin: 40px auto; - background: rgba(255,255,255,0.95); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0,0,0,0.15); - padding: 32px 24px 24px 24px; - text-align: center; -} -.banner { - width: 100%; - border-radius: 12px; - margin-bottom: 16px; + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg); + color: var(--gray-900); + min-height: 100vh; + transition: background 0.2s, color 0.2s; +} + +/* ── Header ─────────────────────────────────────────── */ +header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.5rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + box-shadow: var(--shadow-lg); +} + +.header-left h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; } +.header-left p { font-size: 0.85rem; opacity: 0.8; margin-top: 2px; } + +.header-right { display: flex; align-items: center; gap: 0.6rem; } + +#btn-dark-mode { + background: rgba(255,255,255,.15); + color: #ffffff; + border: 1.5px solid rgba(255,255,255,.3); + border-radius: 8px; + padding: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, transform 0.1s; +} +#btn-dark-mode:hover { background: rgba(255,255,255,.25); transform: translateY(-1px); } + +#btn-add { + background: #ffffff; + color: var(--teal-700); + border: none; + border-radius: 8px; + padding: 0.55rem 1.2rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.4rem; + transition: background 0.15s, transform 0.1s; + white-space: nowrap; +} +#btn-add:hover { background: var(--teal-50); transform: translateY(-1px); } + +/* ── Content area ────────────────────────────────────── */ +.content { + max-width: 820px; + margin: 2rem auto; + padding: 0 1.5rem 3rem; +} + +/* ── Table ───────────────────────────────────────────── */ +#activity-table { + width: 100%; + border-collapse: collapse; + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; + transition: background 0.2s; +} + +#activity-table thead tr { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; +} + +#activity-table th { + padding: 0.85rem 1.1rem; + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.02em; + text-align: left; +} + +#activity-table th.col-actions { text-align: center; } + +#activity-table td { + padding: 0.75rem 1.1rem; + font-size: 0.93rem; + color: var(--gray-900); + border-bottom: 1px solid var(--gray-200); + transition: background 0.15s, color 0.2s, border-color 0.2s; +} + +#activity-table tbody tr:last-child td { border-bottom: none; } +#activity-table tbody tr:hover td { background: var(--teal-50); } + +.col-btn { + width: 1px; + text-align: center; + padding-left: 0.3rem !important; + padding-right: 0.3rem !important; + white-space: nowrap; +} + +#activity-table td.col-btn:last-child { padding-right: 0.6rem !important; } + +/* ── Row action buttons ──────────────────────────────── */ +.btn-edit, .btn-delete { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.55rem 0.75rem; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s, transform 0.1s; + white-space: nowrap; + width: 90px; + justify-content: center; +} + +.btn-edit { + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); +} + +.btn-edit:hover { + background: var(--teal-50); + transform: translateY(-1px); +} + +.btn-delete { + border: none; + background: var(--teal-600); + color: #ffffff; +} + +.btn-delete:hover { + background: var(--teal-700); + transform: translateY(-1px); +} + +/* ── Empty cell ──────────────────────────────────────── */ +.empty-cell { + text-align: center; + color: var(--gray-400) !important; + font-style: italic; + padding: 3rem 1rem !important; +} + +/* ── Modal overlay ───────────────────────────────────── */ +#overlay, #delete-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,.45); + z-index: 100; + align-items: center; + justify-content: center; + padding: 1rem; +} +#overlay.open, #delete-overlay.open { display: flex; } + +.modal { + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 460px; + overflow: hidden; + transition: background 0.2s; +} + +.modal-header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.1rem 1.4rem; + display: flex; + align-items: center; + justify-content: space-between; +} +.modal-header h2 { font-size: 1rem; font-weight: 600; } + +#btn-close-modal, #btn-close-delete-modal { + background: none; + border: none; + color: rgba(255,255,255,.8); + cursor: pointer; + font-size: 1.4rem; + line-height: 1; + padding: 2px; + transition: color 0.15s; +} +#btn-close-modal:hover, #btn-close-delete-modal:hover { color: #ffffff; } + +.modal-body { + padding: 1.4rem; + color: var(--gray-700); + font-size: 0.93rem; + line-height: 1.5; + transition: color 0.2s; +} + +.modal form { + padding: 1.4rem; + display: flex; + flex-direction: column; + gap: 1rem; } + +.field { display: flex; flex-direction: column; gap: 0.3rem; } + +.field label { font-size: 0.82rem; font-weight: 600; color: var(--gray-700); } + +.field input { + padding: 0.55rem 0.8rem; + border: 1.5px solid var(--gray-200); + border-radius: 7px; + font-size: 0.9rem; + font-family: inherit; + color: var(--gray-900); + background: var(--white); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s, background 0.2s, color 0.2s; +} +.field input:focus { + border-color: var(--teal-500); + box-shadow: 0 0 0 3px rgba(14,109,168,.15); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.6rem; + padding: 0 1.4rem 1.4rem; +} + +.btn-secondary { + padding: 0.55rem 1.1rem; + border-radius: 7px; + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s; +} +.btn-secondary:hover { background: var(--teal-50); } + +.btn-primary { + padding: 0.55rem 1.3rem; + border-radius: 7px; + border: none; + background: var(--teal-600); + color: #ffffff; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} +.btn-primary:hover { background: var(--teal-700); } + +/* ── Toast ───────────────────────────────────────────── */ +#toast { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + background: var(--toast-bg); + color: var(--toast-fg); + padding: 0.65rem 1.1rem; + border-radius: 8px; + font-size: 0.85rem; + opacity: 0; + transform: translateY(8px); + pointer-events: none; + transition: opacity 0.2s, transform 0.2s; + z-index: 200; +} +#toast.show { opacity: 1; transform: none; } + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 600px) { + header { padding: 1.2rem 1rem; } + .content { padding: 1rem 0.75rem 3rem; } + .col-btn { white-space: nowrap; } + .btn-edit, .btn-delete { width: auto; padding: 0.55rem 0.5rem; } +} + diff --git a/samples/web-app-managed-identity/python/src/static/summer_banner.jpg b/samples/web-app-managed-identity/python/src/static/summer_banner.jpg deleted file mode 100644 index 2641b9d..0000000 Binary files a/samples/web-app-managed-identity/python/src/static/summer_banner.jpg and /dev/null differ diff --git a/samples/web-app-managed-identity/python/src/templates/index.html b/samples/web-app-managed-identity/python/src/templates/index.html index 7259abe..45a7be8 100644 --- a/samples/web-app-managed-identity/python/src/templates/index.html +++ b/samples/web-app-managed-identity/python/src/templates/index.html @@ -1,49 +1,260 @@ - - Vacation Planner - - - + + + Vacation Planner + + + + + - -
- Summer Banner -

Vacation Planner

-
-
- - -
-
-
- - - - - - - - - {% for activity in activities %} - - - - - {% else %} - - - - {% endfor %} - -
ActivityAction
{{ activity[1] }} -
- - -
-
No vacation plans yet!
-
+ + + +
+
+

🌴 Vacation Planner

+

{{ activities|length }} activit{{ 'ies' if activities|length != 1 else 'y' }} planned

+
+
+ + +
+
+ + +
+ + + + + + + + + {% for activity in activities %} + + + + + + {% else %} + + + + {% endfor %} + +
ActivityActions
{{ activity[1] }} + + +
+ +
+
No vacation plans yet — add your first activity!
+
+ + +
+ +
+ + +
+ +
+ + +
+ + diff --git a/samples/web-app-sql-database/python/images/vacation-planner.png b/samples/web-app-sql-database/python/images/vacation-planner.png index 7a80bb8..a7c5151 100644 Binary files a/samples/web-app-sql-database/python/images/vacation-planner.png and b/samples/web-app-sql-database/python/images/vacation-planner.png differ diff --git a/samples/web-app-sql-database/python/src/app.py b/samples/web-app-sql-database/python/src/app.py index 46c762f..7c1663b 100644 --- a/samples/web-app-sql-database/python/src/app.py +++ b/samples/web-app-sql-database/python/src/app.py @@ -5,10 +5,11 @@ from activities import ActivitiesHelper from certificates import get_certificate_info, get_ssl_context_from_keyvault -from flask import Flask, jsonify, redirect, render_template, request, url_for +from flask import Flask, flash, jsonify, redirect, render_template, request, url_for # Initialize Flask application app: Flask = Flask(__name__) +app.secret_key = os.environ.get('FLASK_SECRET_KEY', os.urandom(24)) # Configure logging logging.basicConfig( @@ -76,6 +77,7 @@ def index(): updated_activity = activities_helper.update_activity_by_id(row_id, activity_text) if updated_activity: + flash('Activity updated.') logger.info(f"Activity updated: {row_id}") else: # Create an activity document with the activity text provided @@ -87,6 +89,7 @@ def index(): if inserted_activity: # Append the activity to the in-memory list activities.append((inserted_activity["id"], inserted_activity["activity"])) + flash('Activity added.') logger.info(f"Activity created: {inserted_activity['id']}") except (ConnectionError, ValueError) as e: logger.error("Error creating/updating activity: %s", e) @@ -114,6 +117,7 @@ def delete(activity_id: int): rows_deleted = activities_helper.delete_activity_by_id(db_activity_id) if rows_deleted > 0: + flash('Activity deleted.') logger.info(f"Activity deleted: {db_activity_id}") else: logger.warning(f"No activity found with ID: {db_activity_id}") diff --git a/samples/web-app-sql-database/python/src/static/style.css b/samples/web-app-sql-database/python/src/static/style.css index 8fb702c..67508fa 100644 --- a/samples/web-app-sql-database/python/src/static/style.css +++ b/samples/web-app-sql-database/python/src/static/style.css @@ -1,98 +1,341 @@ -.action-col { - padding-left: 0 !important; - padding-right: 0 !important; +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --teal-50: #f0fafb; + --teal-100: #d0f0f5; + --teal-500: #0e9db0; + --teal-600: #0e6ba8; + --teal-700: #0a5a8e; + --teal-800: #074d78; + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-700: #374151; + --gray-900: #111827; + --white: #ffffff; + --bg: #f0f8ff; + --shadow-sm: 0 1px 2px rgba(0,0,0,.06); + --shadow: 0 4px 6px -1px rgba(0,0,0,.10), 0 2px 4px -2px rgba(0,0,0,.06); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.10), 0 4px 6px -4px rgba(0,0,0,.06); + --radius: 12px; + --toast-bg: #111827; + --toast-fg: #ffffff; } -.action-col .sea-btn { - margin-left: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; + +html[data-theme="dark"] { + --gray-50: #0f172a; + --gray-100: #1e293b; + --gray-200: #334155; + --gray-400: #94a3b8; + --gray-500: #cbd5e1; + --gray-700: #e2e8f0; + --gray-900: #f8fafc; + --white: #1e293b; + --bg: #0a1929; + --teal-50: #0e2a38; + --teal-700: #7dd3e8; + --shadow-sm: 0 1px 2px rgba(0,0,0,.4); + --shadow: 0 4px 6px -1px rgba(0,0,0,.5), 0 2px 4px -2px rgba(0,0,0,.4); + --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.6), 0 4px 6px -4px rgba(0,0,0,.4); + --toast-bg: #334155; + --toast-fg: #f8fafc; } + body { - background: linear-gradient(135deg, #fff 0%, #a2d4f7 100%); - font-family: 'Segoe UI', sans-serif; - margin: 0; - padding: 0; - min-height: 100vh; -} - -.sea-title { - color: #0e6ba8; -} -.sea-form input[type="text"] { - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - color: #0e6ba8; -} -.sea-form input[type="text"]:focus { - border: 2px solid #0e6ba8; - box-shadow: none; - outline: none; -} -.sea-btn { - background: #fff; - color: #0e6ba8; - border: 2px solid #0e6ba8; - border-radius: 6px; - font-size: 1em; - font-weight: 600; - transition: background 0.2s, color 0.2s; - width: 120px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - margin-left: auto; - padding: 0; -} -.sea-btn:hover { - background: #0e6ba8; - color: #fff; -} -.sea-form { - display: flex; - gap: 0; - justify-content: flex-end; - margin-bottom: 24px; -} -.sea-form .form-control { - margin-right: 10px; -} -.sea-table th, .sea-table td { - border-bottom: 2px solid #0e6ba8 !important; - color: #0e6ba8; -} -.sea-table th { - background: #e3f6fd; -} - -/* Remove left padding for Action column to align header and button */ -.action-col { - padding-left: 0 !important; -} -.sea-table .btn-danger { - border: 2px solid #0e6ba8; - background: #fff; - color: #0e6ba8; -} -.sea-table .btn-danger:hover { - background: #0e6ba8; - color: #fff; -} -.sea-table .text-muted { - color: #7bb7e0 !important; -} -.container { - max-width: 600px; - margin: 40px auto; - background: rgba(255,255,255,0.95); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0,0,0,0.15); - padding: 32px 24px 24px 24px; - text-align: center; -} -.banner { - width: 100%; - border-radius: 12px; - margin-bottom: 16px; + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg); + color: var(--gray-900); + min-height: 100vh; + transition: background 0.2s, color 0.2s; +} + +/* ── Header ─────────────────────────────────────────── */ +header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.5rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + box-shadow: var(--shadow-lg); +} + +.header-left h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; } +.header-left p { font-size: 0.85rem; opacity: 0.8; margin-top: 2px; } + +.header-right { display: flex; align-items: center; gap: 0.6rem; } + +#btn-dark-mode { + background: rgba(255,255,255,.15); + color: #ffffff; + border: 1.5px solid rgba(255,255,255,.3); + border-radius: 8px; + padding: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, transform 0.1s; +} +#btn-dark-mode:hover { background: rgba(255,255,255,.25); transform: translateY(-1px); } + +#btn-add { + background: #ffffff; + color: var(--teal-700); + border: none; + border-radius: 8px; + padding: 0.55rem 1.2rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.4rem; + transition: background 0.15s, transform 0.1s; + white-space: nowrap; +} +#btn-add:hover { background: var(--teal-50); transform: translateY(-1px); } + +/* ── Content area ────────────────────────────────────── */ +.content { + max-width: 820px; + margin: 2rem auto; + padding: 0 1.5rem 3rem; +} + +/* ── Table ───────────────────────────────────────────── */ +#activity-table { + width: 100%; + border-collapse: collapse; + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; + transition: background 0.2s; +} + +#activity-table thead tr { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; +} + +#activity-table th { + padding: 0.85rem 1.1rem; + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.02em; + text-align: left; +} + +#activity-table th.col-actions { text-align: center; } + +#activity-table td { + padding: 0.75rem 1.1rem; + font-size: 0.93rem; + color: var(--gray-900); + border-bottom: 1px solid var(--gray-200); + transition: background 0.15s, color 0.2s, border-color 0.2s; +} + +#activity-table tbody tr:last-child td { border-bottom: none; } +#activity-table tbody tr:hover td { background: var(--teal-50); } + +.col-btn { + width: 1px; + text-align: center; + padding-left: 0.3rem !important; + padding-right: 0.3rem !important; + white-space: nowrap; +} + +#activity-table td.col-btn:last-child { padding-right: 0.6rem !important; } + +/* ── Row action buttons ──────────────────────────────── */ +.btn-edit, .btn-delete { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.55rem 0.75rem; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s, transform 0.1s; + white-space: nowrap; + width: 90px; + justify-content: center; +} + +.btn-edit { + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); +} + +.btn-edit:hover { + background: var(--teal-50); + transform: translateY(-1px); +} + +.btn-delete { + border: none; + background: var(--teal-600); + color: #ffffff; +} + +.btn-delete:hover { + background: var(--teal-700); + transform: translateY(-1px); +} + +/* ── Empty cell ──────────────────────────────────────── */ +.empty-cell { + text-align: center; + color: var(--gray-400) !important; + font-style: italic; + padding: 3rem 1rem !important; +} + +/* ── Modal overlay ───────────────────────────────────── */ +#overlay, #delete-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,.45); + z-index: 100; + align-items: center; + justify-content: center; + padding: 1rem; +} +#overlay.open, #delete-overlay.open { display: flex; } + +.modal { + background: var(--white); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 460px; + overflow: hidden; + transition: background 0.2s; +} + +.modal-header { + background: linear-gradient(135deg, var(--teal-800) 0%, var(--teal-600) 100%); + color: #ffffff; + padding: 1.1rem 1.4rem; + display: flex; + align-items: center; + justify-content: space-between; +} +.modal-header h2 { font-size: 1rem; font-weight: 600; } + +#btn-close-modal, #btn-close-delete-modal { + background: none; + border: none; + color: rgba(255,255,255,.8); + cursor: pointer; + font-size: 1.4rem; + line-height: 1; + padding: 2px; + transition: color 0.15s; +} +#btn-close-modal:hover, #btn-close-delete-modal:hover { color: #ffffff; } + +.modal-body { + padding: 1.4rem; + color: var(--gray-700); + font-size: 0.93rem; + line-height: 1.5; + transition: color 0.2s; +} + +.modal form { + padding: 1.4rem; + display: flex; + flex-direction: column; + gap: 1rem; } + +.field { display: flex; flex-direction: column; gap: 0.3rem; } + +.field label { font-size: 0.82rem; font-weight: 600; color: var(--gray-700); } + +.field input { + padding: 0.55rem 0.8rem; + border: 1.5px solid var(--gray-200); + border-radius: 7px; + font-size: 0.9rem; + font-family: inherit; + color: var(--gray-900); + background: var(--white); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s, background 0.2s, color 0.2s; +} +.field input:focus { + border-color: var(--teal-500); + box-shadow: 0 0 0 3px rgba(14,109,168,.15); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.6rem; + padding: 0 1.4rem 1.4rem; +} + +.btn-secondary { + padding: 0.55rem 1.1rem; + border-radius: 7px; + border: 1.5px solid var(--teal-700); + background: var(--white); + color: var(--teal-700); + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.2s, border-color 0.2s; +} +.btn-secondary:hover { background: var(--teal-50); } + +.btn-primary { + padding: 0.55rem 1.3rem; + border-radius: 7px; + border: none; + background: var(--teal-600); + color: #ffffff; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} +.btn-primary:hover { background: var(--teal-700); } + +/* ── Toast ───────────────────────────────────────────── */ +#toast { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + background: var(--toast-bg); + color: var(--toast-fg); + padding: 0.65rem 1.1rem; + border-radius: 8px; + font-size: 0.85rem; + opacity: 0; + transform: translateY(8px); + pointer-events: none; + transition: opacity 0.2s, transform 0.2s; + z-index: 200; +} +#toast.show { opacity: 1; transform: none; } + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 600px) { + header { padding: 1.2rem 1rem; } + .content { padding: 1rem 0.75rem 3rem; } + .col-btn { white-space: nowrap; } + .btn-edit, .btn-delete { width: auto; padding: 0.55rem 0.5rem; } +} + diff --git a/samples/web-app-sql-database/python/src/static/summer_banner.jpg b/samples/web-app-sql-database/python/src/static/summer_banner.jpg deleted file mode 100644 index 2641b9d..0000000 Binary files a/samples/web-app-sql-database/python/src/static/summer_banner.jpg and /dev/null differ diff --git a/samples/web-app-sql-database/python/src/templates/index.html b/samples/web-app-sql-database/python/src/templates/index.html index d9e0cfa..45a7be8 100644 --- a/samples/web-app-sql-database/python/src/templates/index.html +++ b/samples/web-app-sql-database/python/src/templates/index.html @@ -1,69 +1,260 @@ - - Vacation Planner - - - + + + Vacation Planner + + + + + - -
- Summer Banner -

Vacation Planner

-
-
- - - -
-
-
- - - - - - - - - {% for activity in activities %} - - - - - - {% else %} - - - - {% endfor %} - -
ActivityAction
{{ activity[1] }} - Edit - -
- - -
-
No vacation plans yet!
-
+ + + +
+
+

🌴 Vacation Planner

+

{{ activities|length }} activit{{ 'ies' if activities|length != 1 else 'y' }} planned

+
+
+ + +
+
+ + +
+ + + + + + + + + {% for activity in activities %} + + + + + + {% else %} + + + + {% endfor %} + +
ActivityActions
{{ activity[1] }} + + +
+ +
+
No vacation plans yet — add your first activity!
+
+ + +
+ +
+ + +
+ +
+ + +
+ +