diff --git a/build_editor.py b/build_editor.py index 597412d..5bd58d1 100644 --- a/build_editor.py +++ b/build_editor.py @@ -7,6 +7,13 @@ OUT = BASE / "templates" / "editor.html" content = SRC.read_text(encoding="utf-8") +# ── Normalize line endings to LF to avoid CRLF mismatch on Windows ── +content = content.replace("\r\n", "\n") + +# ── Correct themeBaseUrl and mediaPath in editor.html ── +content = content.replace("Vvveb.themeBaseUrl = 'demo/landing/';", "Vvveb.themeBaseUrl = '/static/Vvvebjs/demo/landing/';") +content = content.replace("window.mediaPath = '../../media';", "window.mediaPath = '/static/Vvvebjs/media/';") + # ── 1. Fix static asset paths ──────────────────────────────────── VBASE = "/static/Vvvebjs/" replacements = [ @@ -41,6 +48,20 @@ JS_VARS = [ for old, new in JS_VARS: content = content.replace(old, new) +# ── Make editor load requested page from query parameter ── +old_init = 'let firstPage = Object.keys(pages)[0];\nVvveb.Builder.init(pages[firstPage]["url"], function () {' +new_init = """let firstPage = Object.keys(pages)[0]; +const urlParams = new URLSearchParams(window.location.search); +const requestedPage = urlParams.get('page'); +if (requestedPage) { +\tlet pageKey = requestedPage.replace(".html", ""); +\tif (pages[pageKey]) { +\t\tfirstPage = pageKey; +\t} +} +Vvveb.Builder.init(pages[firstPage]["url"], function () {""" +content = content.replace(old_init, new_init) + # ── 4. Wrap everything in {% raw %} ... {% endraw %} to avoid Jinja2 parsing conflicts ── # We do this first so Jinja2 ignores VvvebJS's frontend micro-templates ({%= %}, {% %}). # Then we selectively exit the {% raw %} block using {% endraw %} ... {% raw %} for our dynamic values. @@ -94,7 +115,7 @@ BACK_BTN = """ // Inject project slug for save bridge window.VVVEB_PROJECT_SLUG = "{% endraw %}{{ slug | safe }}{% raw %}"; - + """ content = content.replace("", BACK_BTN + "\n") diff --git a/main.py b/main.py index 6c95d14..e45b286 100644 --- a/main.py +++ b/main.py @@ -263,12 +263,78 @@ def api_save() -> tuple[Response, int] | Response: body = {k: (v[0] if isinstance(v, list) else v) for k, v in form.items()} slug: str = str(body.get("slug", "")).strip() + if not slug: + # Fallback to query parameter if not in body (e.g. for some raw requests) + slug = request.args.get("slug", "").strip() + if not slug: return jsonify({"error": "缺少專案識別碼 (slug)"}), 400 if not _project_dir(slug).exists(): return jsonify({"error": "專案不存在"}), 404 + action = request.args.get("action", "").strip() + + # ── 處理重新命名 / 複製頁面 ── + if action == "rename": + file = str(body.get("file", "")).strip() + newfile = str(body.get("newfile", "")).strip() + duplicate_str = str(body.get("duplicate", "")).strip().lower() + is_duplicate = (duplicate_str == "true") + + if not file or not newfile: + return jsonify({"error": "缺少參數 file 或 newfile"}), 400 + + old_path = _sanitize_file_path(slug, file) + new_path = _sanitize_file_path(slug, newfile) + + if old_path is None or new_path is None: + return jsonify({"error": "不合法的頁面名稱"}), 400 + + if not old_path.exists(): + return jsonify({"error": "來源頁面不存在"}), 404 + + if new_path.exists() and old_path != new_path: + return jsonify({"error": "目標頁面已存在"}), 409 + + if is_duplicate: + shutil.copy(old_path, new_path) + msg = "頁面複製成功" + else: + old_path.rename(new_path) + msg = "頁面重新命名成功" + + return jsonify({ + "success": True, + "ok": True, + "message": msg, + "newfile": new_path.name, + "url": f"/sites/{slug}/{new_path.name}" + }) + + # ── 處理刪除頁面 ── + elif action == "delete": + file = str(body.get("file", "")).strip() + if not file: + return jsonify({"error": "缺少參數 file"}), 400 + + if file.lower() == "index.html": + return jsonify({"error": "無法刪除主頁 index.html"}), 400 + + safe_path = _sanitize_file_path(slug, file) + if safe_path is None: + return jsonify({"error": "不合法的頁面名稱"}), 400 + + if not safe_path.exists(): + return jsonify({"error": "頁面不存在"}), 404 + + safe_path.unlink() + return jsonify({ + "success": True, + "ok": True, + "message": "頁面已成功刪除" + }) + # 1. 判斷是否為新增頁面請求 (含有 startTemplateUrl) start_template_url = str(body.get("startTemplateUrl", "")).strip() if start_template_url: diff --git a/static/js/my-editor.js b/static/js/my-editor.js index 49fc709..a81eabb 100644 --- a/static/js/my-editor.js +++ b/static/js/my-editor.js @@ -1,6 +1,6 @@ /** * my-editor.js — VvvebJS 儲存與新增頁面橋接腳本 - * 覆蓋 Vvveb.Builder.saveAjax 行為,使儲存與新增頁面功能無縫串接至 Flask 後端 + * 覆蓋 Vvveb.Builder.saveAjax 與 Vvveb.FileManager 行為,無縫串接到 Flask 後端,並引入精美網頁型暗黑系模態框 */ (function () { "use strict"; @@ -49,10 +49,292 @@ }, 3000); } + // ── 頂級網頁型模態框 (Web-based Custom Dialogs) ────────────────────── + function showModalPrompt(title, defaultValue, callback) { + const existing = document.getElementById("vvveb-custom-modal"); + if (existing) existing.remove(); + + const overlay = document.createElement("div"); + overlay.id = "vvveb-custom-modal"; + Object.assign(overlay.style, { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + zIndex: "100000", + background: "rgba(10, 11, 18, 0.65)", + backdropFilter: "blur(12px)", + webkitBackdropFilter: "blur(12px)", + display: "flex", + alignItems: "center", + justifyContent: "center", + opacity: "0", + transition: "opacity 0.25s ease-out" + }); + + const card = document.createElement("div"); + Object.assign(card.style, { + background: "rgba(22, 24, 38, 0.95)", + border: "1px solid rgba(255, 255, 255, 0.08)", + boxShadow: "0 20px 50px rgba(0, 0, 0, 0.6), 0 0 40px rgba(99, 102, 241, 0.1)", + borderRadius: "16px", + width: "420px", + padding: "2rem", + color: "#f8fafc", + fontFamily: "Inter, system-ui, sans-serif", + transform: "scale(0.95)", + transition: "transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)", + display: "flex", + flexDirection: "column", + gap: "1.25rem" + }); + + card.innerHTML = ` +
+ + + + + ${title} +
+
+ +
+
+ + +
+ `; + + overlay.appendChild(card); + document.body.appendChild(overlay); + + const input = card.querySelector("#vvveb-modal-input"); + input.value = defaultValue; + input.addEventListener("focus", () => { + input.style.borderColor = "#818cf8"; + input.style.boxShadow = "0 0 12px rgba(129, 140, 248, 0.25)"; + }); + input.addEventListener("blur", () => { + input.style.borderColor = "rgba(255, 255, 255, 0.1)"; + input.style.boxShadow = "none"; + }); + + const cancelBtn = card.querySelector("#vvveb-modal-cancel"); + cancelBtn.addEventListener("mouseover", () => { + cancelBtn.style.background = "rgba(255, 255, 255, 0.05)"; + cancelBtn.style.color = "#fff"; + }); + cancelBtn.addEventListener("mouseout", () => { + cancelBtn.style.background = "transparent"; + cancelBtn.style.color = "#94a3b8"; + }); + + const confirmBtn = card.querySelector("#vvveb-modal-confirm"); + confirmBtn.addEventListener("mouseover", () => { + confirmBtn.style.background = "#4338ca"; + }); + confirmBtn.addEventListener("mouseout", () => { + confirmBtn.style.background = "#4f46e5"; + }); + + // Fade in + setTimeout(() => { + overlay.style.opacity = "1"; + card.style.transform = "scale(1)"; + }, 10); + + input.focus(); + input.select(); + + function close(value) { + overlay.style.opacity = "0"; + card.style.transform = "scale(0.95)"; + setTimeout(() => { + overlay.remove(); + if (value !== undefined) { + const finalVal = value.trim ? value.trim() : value; + callback(finalVal); + } + }, 250); + } + + cancelBtn.onclick = () => close(); + confirmBtn.onclick = () => close(input.value); + + input.onkeydown = (e) => { + if (e.key === "Enter") { + close(input.value); + } else if (e.key === "Escape") { + close(); + } + }; + } + + function showModalConfirm(title, message, callback) { + const existing = document.getElementById("vvveb-custom-modal"); + if (existing) existing.remove(); + + const overlay = document.createElement("div"); + overlay.id = "vvveb-custom-modal"; + Object.assign(overlay.style, { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + zIndex: "100000", + background: "rgba(10, 11, 18, 0.65)", + backdropFilter: "blur(12px)", + webkitBackdropFilter: "blur(12px)", + display: "flex", + alignItems: "center", + justifyContent: "center", + opacity: "0", + transition: "opacity 0.25s ease-out" + }); + + const card = document.createElement("div"); + Object.assign(card.style, { + background: "rgba(22, 24, 38, 0.95)", + border: "1px solid rgba(255, 255, 255, 0.08)", + boxShadow: "0 20px 50px rgba(0, 0, 0, 0.6), 0 0 40px rgba(239, 68, 68, 0.1)", + borderRadius: "16px", + width: "420px", + padding: "2rem", + color: "#f8fafc", + fontFamily: "Inter, system-ui, sans-serif", + transform: "scale(0.95)", + transition: "transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)", + display: "flex", + flexDirection: "column", + gap: "1.25rem" + }); + + card.innerHTML = ` +
+ + + + + + ${title} +
+
+ ${message} +
+
+ + +
+ `; + + overlay.appendChild(card); + document.body.appendChild(overlay); + + const cancelBtn = card.querySelector("#vvveb-modal-cancel"); + cancelBtn.addEventListener("mouseover", () => { + cancelBtn.style.background = "rgba(255, 255, 255, 0.05)"; + cancelBtn.style.color = "#fff"; + }); + cancelBtn.addEventListener("mouseout", () => { + cancelBtn.style.background = "transparent"; + cancelBtn.style.color = "#94a3b8"; + }); + + const confirmBtn = card.querySelector("#vvveb-modal-confirm"); + confirmBtn.addEventListener("mouseover", () => { + confirmBtn.style.background = "#dc2626"; + }); + confirmBtn.addEventListener("mouseout", () => { + confirmBtn.style.background = "#ef4444"; + }); + + // Fade in + setTimeout(() => { + overlay.style.opacity = "1"; + card.style.transform = "scale(1)"; + }, 10); + + confirmBtn.focus(); + + function close(confirmed) { + overlay.style.opacity = "0"; + card.style.transform = "scale(0.95)"; + setTimeout(() => { + overlay.remove(); + if (confirmed) callback(); + }, 250); + } + + cancelBtn.onclick = () => close(false); + confirmBtn.onclick = () => close(true); + + const keyHandler = (e) => { + if (e.key === "Escape") { + close(false); + window.removeEventListener("keydown", keyHandler); + } + }; + window.addEventListener("keydown", keyHandler); + } + // ── 覆蓋 Vvveb.Builder.saveAjax ────────────────────────────────── function patchSaveAjax() { if (typeof Vvveb === "undefined" || !Vvveb.Builder) { - // 等待 Vvveb 載入 setTimeout(patchSaveAjax, 200); return; } @@ -73,7 +355,7 @@ } data.slug = SLUG; - // 扁平化處理新檔案檔名(確保所有頁面皆存於專案根目錄下) + // 扁平化處理新檔案檔名 if (data.file) { const parts = data.file.split("/"); data.file = parts[parts.length - 1]; @@ -89,7 +371,7 @@ } } - // 顯示儲存中狀態(一般儲存時觸發) + // 顯示儲存中狀態 const saveBtn = document.querySelector(".save-btn"); if (saveBtn && !isNewPage) { saveBtn.querySelector(".loading")?.classList.remove("d-none"); @@ -147,6 +429,243 @@ console.log("[my-editor] Robust Vvveb.Builder.saveAjax patched for slug:", SLUG); } + // ── 覆蓋 Vvveb.FileManager 頁面操作 ────────────────────────────── + function patchFileManager() { + if (typeof Vvveb === "undefined" || !Vvveb.FileManager) { + setTimeout(patchFileManager, 200); + return; + } + + // 覆蓋 init 方法以修正 VvvebJS 使用全球廢棄的 `event.target` 導致在某些瀏覽器/嚴格模式下點擊無效的 Bug + const originalInit = Vvveb.FileManager.init; + Vvveb.FileManager.init = function (allowedComponents = {}) { + originalInit.call(this, allowedComponents); + + // 利用 Clone 移除舊有包含 Bug 的匿名字選區接聽器 + const newTree = this.tree.cloneNode(true); + this.tree.parentNode.replaceChild(newTree, this.tree); + this.tree = newTree; + + // 綁定符合標準且高度相容 (e.target) 的新接聽器 + this.tree.addEventListener("click", function (e) { + let element = e.target.closest("a"); + if (element) { + e.stopImmediatePropagation(); + if (element.classList.contains('view')) return; + e.preventDefault(); + return false; + } + }); + + this.tree.addEventListener("click", function (e) { + let element = e.target.closest(".delete"); + if (element) { + Vvveb.FileManager.deletePage(element.closest("li"), e); + e.stopImmediatePropagation(); + e.preventDefault(); + return false; + } + }); + + this.tree.addEventListener("click", function (e) { + let element = e.target.closest(".rename"); + if (element) { + Vvveb.FileManager.renamePage(element.closest("li"), e, false); + e.stopImmediatePropagation(); + e.preventDefault(); + return false; + } + }); + + this.tree.addEventListener("click", function (e) { + let element = e.target.closest(".duplicate"); + if (element) { + Vvveb.FileManager.renamePage(element.closest("li"), e, true); + e.stopImmediatePropagation(); + e.preventDefault(); + return false; + } + }); + + this.tree.addEventListener("click", function (e) { + let element = e.target.closest("li[data-page] label"); + if (element) { + let page = element.parentNode.dataset.page; + if (page) { + Vvveb.FileManager.loadPage(page); + e.stopImmediatePropagation(); + e.preventDefault(); + return false; + } + } + }); + }; + + // 覆蓋重新命名 / 複製頁面 + Vvveb.FileManager.renamePage = function (element, e, duplicate = false) { + let page = element.dataset; + showModalPrompt(`請輸入 "${page.file}" 的新檔名:`, page.file, function (newfile) { + if (!newfile) return; + + // 確保副檔名為 .html + if (!newfile.endsWith(".html")) { + newfile += ".html"; + } + + const bodyData = { + slug: SLUG, + file: page.file, + newfile: newfile, + duplicate: duplicate ? "true" : "false" + }; + + fetch(`/api/save?action=rename`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(bodyData).toString() + }) + .then((res) => { + if (!res.ok) { + return res.json().then((err) => { + throw new Error(err.error || "操作失敗"); + }); + } + return res.json(); + }) + .then((data) => { + showToast(`✓ ${data.message}`); + let baseName = data.newfile.replace('.html', ''); + let newName = friendlyName(data.newfile.replace(/.*[\/\\]+/, '')).replace('.html', ''); + + if (duplicate) { + // 複製頁面:在 FileManager 中加入新頁面 + let pageData = Object.assign({}, Vvveb.FileManager.pages[page.page]); + pageData["file"] = data.newfile; + pageData["title"] = newName; + pageData["url"] = data.url; + pageData["name"] = baseName; + Vvveb.FileManager.addPage(baseName, pageData); + } else { + // 重新命名:更新現有節點資訊 + const oldPageKey = page.page; + Vvveb.FileManager.pages[oldPageKey]["file"] = data.newfile; + Vvveb.FileManager.pages[oldPageKey]["title"] = newName; + Vvveb.FileManager.pages[oldPageKey]["url"] = data.url; + Vvveb.FileManager.pages[oldPageKey]["name"] = baseName; + + let link = element.querySelector("a.view"); + if (link) { + link.setAttribute("href", data.url); + } + let span = element.querySelector("label > span"); + if (!span) { + span = element.querySelector("span"); + } + if (span) { + span.textContent = newName; + } + element.dataset.file = data.newfile; + element.dataset.page = baseName; + + // 將 key 重新綁定 + Vvveb.FileManager.pages[baseName] = Vvveb.FileManager.pages[oldPageKey]; + if (baseName !== oldPageKey) { + delete Vvveb.FileManager.pages[oldPageKey]; + } + } + }) + .catch((err) => { + showToast(`操作失敗:${err.message}`, "error"); + }); + }); + }; + + // 覆蓋刪除頁面 + Vvveb.FileManager.deletePage = function (element, e) { + let page = element.dataset; + if (page.file.toLowerCase() === "index.html") { + showToast("錯誤:無法刪除主頁 index.html", "error"); + return; + } + + showModalConfirm("確認刪除頁面", `確定要刪除 "${page.file}" 頁面嗎?此動作將無法復原。`, function () { + const bodyData = { + slug: SLUG, + file: page.file + }; + + fetch(`/api/save?action=delete`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(bodyData).toString() + }) + .then((res) => { + if (!res.ok) { + return res.json().then((err) => { + throw new Error(err.error || "刪除失敗"); + }); + } + return res.json(); + }) + .then((data) => { + showToast(`✓ ${data.message}`); + + // 從內部 pages 列表中移除 + delete Vvveb.FileManager.pages[page.page]; + element.remove(); + + // 如果刪除的是目前正在編輯的頁面,切換回第一個頁面 + if (Vvveb.FileManager.currentPage === page.page) { + let firstPage = Object.keys(Vvveb.FileManager.pages)[0]; + if (firstPage) { + Vvveb.FileManager.loadPage(firstPage); + } + } + }) + .catch((err) => { + showToast(`刪除失敗:${err.message}`, "error"); + }); + }); + }; + + console.log("[my-editor] Vvveb.FileManager operations patched."); + } + + // ── 覆蓋 Vvveb.NewSection 防止佈局崩潰 ───────────────────────────── + function patchNewSection() { + if (typeof Vvveb === "undefined" || !Vvveb.NewSection) { + setTimeout(patchNewSection, 200); + return; + } + + Vvveb.NewSection.insert = function () { + let position = 'before'; + let lastSection = Vvveb.Builder.frameBody.querySelector(':scope > footer:last-of-type'); + if (!lastSection) { + position = 'after'; + lastSection = Vvveb.Builder.frameBody.querySelector(':scope > main:last-of-type') || + Vvveb.Builder.frameBody.querySelector(':scope > section:last-of-type') || + Vvveb.Builder.frameBody.querySelector(':scope > div:last-of-type') || + Vvveb.Builder.frameBody; // 最安全的回退點:整個 body 節點 + } + + this.container = generateElements(this.template)[0]; + if (lastSection === Vvveb.Builder.frameBody) { + Vvveb.Builder.frameBody.appendChild(this.container); + } else { + if (position == 'after') { + lastSection.after(this.container); + } else { + lastSection.before(this.container); + } + } + + return this.container; + }; + + console.log("[my-editor] Vvveb.NewSection.insert patched to prevent layout crashes."); + } + // ── 啟用 Save 按鈕(vvvebjs 預設 disabled)────────────────────── function enableSaveBtn() { const btn = document.querySelector(".save-btn"); @@ -177,10 +696,14 @@ if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { patchSaveAjax(); + patchFileManager(); + patchNewSection(); enableSaveBtn(); }); } else { patchSaveAjax(); + patchFileManager(); + patchNewSection(); enableSaveBtn(); } })(); diff --git a/templates/dashboard.html b/templates/dashboard.html index 0896714..acc3d10 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -402,12 +402,21 @@ async function openSettings(slug) { listEl.innerHTML = '
  • 尚無頁面
  • '; } else { listEl.innerHTML = pages.map(p => ` -
  • - - - - - ${escHtml(p)} +
  • +
    + + + + + ${escHtml(p)} +
    + + + + + + 編輯 +
  • `).join(''); } } catch(e) { diff --git a/templates/editor.html b/templates/editor.html index 36f2a90..eb3f4ea 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -2165,8 +2165,8 @@ + + + + + + + + +
    +
    +
    +
    +
    + +
    +
    +
    + nudoragon Avatar +
    +
    +

    nudoragon

    +

    設計與開發的探索者 | 建立數位世界的連結

    +
    + + + + +
    + + + + + + \ No newline at end of file diff --git a/websites/linktree-test/project.json b/websites/linktree-test/project.json new file mode 100644 index 0000000..53dbfe2 --- /dev/null +++ b/websites/linktree-test/project.json @@ -0,0 +1,6 @@ +{ + "name": "linktree-test", + "slug": "linktree-test", + "description": "", + "created_at": "2026-05-18T04:37:34" +} \ No newline at end of file diff --git a/websites/linktree-test/style.css b/websites/linktree-test/style.css new file mode 100644 index 0000000..bc4bb35 --- /dev/null +++ b/websites/linktree-test/style.css @@ -0,0 +1,297 @@ +/* CSS Variables & Design Tokens */ +:root { + --primary-color: #6366f1; + --accent-color: #a855f7; + --bg-dark: #0f172a; + --card-bg: rgba(30, 41, 59, 0.7); + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --glass-border: rgba(255, 255, 255, 0.1); + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Base Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; + background-color: var(--bg-dark); + color: var(--text-primary); + line-height: 1.5; + overflow-x: hidden; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 2rem 1rem; +} + +/* Background Blobs */ +.background-blobs { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + overflow: hidden; +} + +.blob { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.4; + animation: blob-float 20s infinite alternate; +} + +.blob-1 { + width: 400px; + height: 400px; + background: var(--primary-color); + top: -100px; + left: -100px; +} + +.blob-2 { + width: 300px; + height: 300px; + background: var(--accent-color); + bottom: -50px; + right: -50px; + animation-delay: -5s; +} + +.blob-3 { + width: 250px; + height: 250px; + background: #ec4899; + top: 40%; + left: 60%; + animation-delay: -10s; +} + +@keyframes blob-float { + 0% { transform: translate(0, 0) scale(1); } + 100% { transform: translate(50px, 50px) scale(1.1); } +} + +/* Container */ +.container { + width: 100%; + max-width: 580px; + text-align: center; + position: relative; + z-index: 1; +} + +/* Profile Section */ +.profile { + margin-bottom: 3rem; +} + +.avatar-container { + position: relative; + width: 120px; + height: 120px; + margin: 0 auto 1.5rem; +} + +.avatar { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + border: 4px solid var(--glass-border); + position: relative; + z-index: 2; +} + +.avatar-ring { + position: absolute; + top: -8px; + left: -8px; + right: -8px; + bottom: -8px; + border-radius: 50%; + background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); + opacity: 0.5; + z-index: 1; + animation: ring-pulse 3s infinite; +} + +@keyframes ring-pulse { + 0% { transform: scale(1); opacity: 0.5; } + 50% { transform: scale(1.05); opacity: 0.2; } + 100% { transform: scale(1); opacity: 0.5; } +} + +.name { + font-family: 'Outfit', sans-serif; + font-size: 2.2rem; + font-weight: 800; + margin-bottom: 0.5rem; + letter-spacing: -0.02em; +} + +.bio { + color: var(--text-secondary); + font-size: 1.1rem; + max-width: 80%; + margin: 0 auto; +} + +/* Links Section */ +.links-container { + display: flex; + flex-direction: column; + gap: 1.2rem; + margin-bottom: 3rem; +} + +.link-card { + display: flex; + align-items: center; + background: var(--card-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: 1.2rem; + padding: 1.2rem; + text-decoration: none; + color: inherit; + transition: var(--transition); + text-align: left; +} + +.link-card:hover { + transform: translateY(-5px) scale(1.02); + background: rgba(45, 55, 72, 0.8); + border-color: rgba(255, 255, 255, 0.2); + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3); +} + +.link-icon { + width: 50px; + height: 50px; + background: rgba(255, 255, 255, 0.1); + border-radius: 0.8rem; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.5rem; + margin-right: 1.2rem; + color: var(--primary-color); + transition: var(--transition); +} + +.link-card:hover .link-icon { + background: var(--primary-color); + color: white; +} + +.link-content { + flex: 1; +} + +.link-title { + display: block; + font-weight: 600; + font-size: 1.1rem; + margin-bottom: 0.2rem; +} + +.link-desc { + display: block; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.link-arrow { + color: var(--text-secondary); + opacity: 0.5; + transition: var(--transition); +} + +.link-card:hover .link-arrow { + transform: translateX(5px); + opacity: 1; + color: var(--primary-color); +} + +/* Footer Section */ +.footer { + padding-bottom: 2rem; +} + +.social-icons { + display: flex; + justify-content: center; + gap: 2rem; + margin-bottom: 1.5rem; +} + +.social-icons a { + color: var(--text-secondary); + font-size: 1.8rem; + transition: var(--transition); +} + +.social-icons a:hover { + color: var(--primary-color); + transform: scale(1.2); +} + +.copyright { + color: var(--text-secondary); + font-size: 0.85rem; + opacity: 0.6; +} + +/* Animations */ +.animate-fade-in { + opacity: 0; + animation: fade-in 1s forwards; +} + +.animate-slide-up { + opacity: 0; + transform: translateY(20px); + animation: slide-up 0.8s forwards; +} + +@keyframes fade-in { + to { opacity: 1; } +} + +@keyframes slide-up { + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Adjustments */ +@media (max-width: 480px) { + body { + padding: 1.5rem 0.8rem; + } + + .name { + font-size: 1.8rem; + } + + .bio { + font-size: 1rem; + } + + .link-icon { + width: 44px; + height: 44px; + font-size: 1.2rem; + } +} diff --git a/websites/my-website/about-us.html b/websites/my-website/about-final.html similarity index 91% rename from websites/my-website/about-us.html rename to websites/my-website/about-final.html index 968063a..79e902e 100644 --- a/websites/my-website/about-us.html +++ b/websites/my-website/about-final.html @@ -4,7 +4,7 @@ - My page + About us @@ -23,11 +23,10 @@
    -

    Bootstrap 5 start page

    +

    About Us page

    Start by dragging components to page or double click to edit text

    - \ No newline at end of file diff --git a/websites/my-website/index.html b/websites/my-website/index.html index 09ddd9c..6d5c261 100644 --- a/websites/my-website/index.html +++ b/websites/my-website/index.html @@ -34,7 +34,7 @@ Home