-
<!DOCTYPE html>
Comfort Table — طعم يشبه المنزل 🍲 *, *::before, *::after { box-sizing: border-box; } :root { --cream: #fdf8f0; --warm-white: #faf3e8; --white: #ffffff; --brown-dark: #3d2a1e; --brown-medium: #6b4c3b; --brown-light: #a8877a; --gold: #d4a373; --gold-light: #f0d5b0; --orange-warm: #e07c4a; --border: rgba(107, 76, 59, .12); --shadow: 0 8px 30px rgba(61, 42, 30, .12); --shadow-hover: 0 12px 40px rgba(61, 42, 30, .20); --radius: 20px; --radius-sm: 12px; --transition: .3s cubic-bezier(.4,0,.2,1); --font-serif: Georgia, "Times New Roman", serif; --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Tahoma, Arial, sans-serif; } html { scroll-behavior: smooth; } body { margin: 0; font-family: var(--font-sans); background: var(--cream); color: var(--brown-dark); line-height: 1.6; overflow-x: hidden; } body.modal-open { overflow: hidden; } button, a { font: inherit; } button { -webkit-tap-highlight-color: transparent; } a { color: inherit; } img { max-width: 100%; display: block; } .container { width: min(1200px, calc(100% - 48px)); margin-inline: auto; } .sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; } .skip-link { position: fixed; top: -100px; right: 50%; transform: translateX(50%); z-index: 10000; background: var(--brown-dark); color: var(--cream); padding: 12px 24px; border-radius: 0 0 var(--radius-sm) var(--radius-sm); text-decoration: none; font-weight: 700; } .skip-link:focus { top: 0; } h1, h2, h3 { font-family: var(--font-serif); line-height: 1.2; letter-spacing: -.02em; margin-top: 0; } h1 { font-size: clamp(2.6rem, 8vw, 5rem); margin-bottom: 0; } h2 { font-size: clamp(2rem, 5vw, 3.2rem); margin-bottom: .5rem; } h3 { font-size: clamp(1.4rem, 3vw, 2rem); } .highlight { color: var(--orange-warm); } .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; padding: 14px 30px; border: 0; border-radius: 999px; cursor: pointer; text-decoration: none; font-weight: 700; transition: transform var(--transition), background var(--transition), color var(--transition), box-shadow var(--transition); } .btn:hover { transform: translateY(-2px); } .btn-primary { background: var(--brown-dark); color: var(--cream); box-shadow: var(--shadow); } .btn-primary:hover, .btn-primary:focus-visible { background: var(--orange-warm); box-shadow: var(--shadow-hover); } .btn-secondary { background: var(--gold); color: var(--brown-dark); } .btn-secondary:hover, .btn-secondary:focus-visible { background: var(--gold-light); } .btn-ghost { background: transparent; color: var(--brown-dark); border: 2px solid var(--brown-light); } .btn-ghost:hover, .btn-ghost:focus-visible { background: var(--brown-dark); color: var(--cream); border-color: var(--brown-dark); } :focus-visible { outline: 3px solid var(--orange-warm); outline-offset: 3px; } /* HEADER */ header { position: sticky; top: 0; z-index: 1000; padding: 18px 0; background: rgba(253,248,240,.94); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border); } .nav-container { display: flex; align-items: center; justify-content: space-between; gap: 20px; } .logo { display: inline-flex; align-items: center; gap: 10px; font: 700 1.4rem var(--font-serif); text-decoration: none; white-space: nowrap; } .logo-icon { font-size: 1.8rem; } .logo-highlight { color: var(--orange-warm); } nav { position: relative; } nav ul { display: flex; align-items: center; gap: 30px; list-style: none; padding: 0; margin: 0; } nav a { position: relative; text-decoration: none; font-weight: 600; color: var(--brown-dark); padding: 6px 0; } nav a::after { content: ""; position: absolute; right: 0; bottom: 0; width: 0; height: 2px; background: var(--orange-warm); transition: width var(--transition); } nav a:hover::after, nav a:focus-visible::after, nav a[aria-current="page"]::after { width: 100%; } nav a[aria-current="page"] { color: var(--orange-warm); } .nav-toggle { display: none; border: 0; background: transparent; padding: 10px; cursor: pointer; border-radius: 10px; } .hamburger, .hamburger::before, .hamburger::after { display: block; width: 28px; height: 3px; border-radius: 3px; background: var(--brown-dark); } .hamburger { position: relative; } .hamburger::before, .hamburger::after { content: ""; position: absolute; right: 0; } .hamburger::before { top: -8px; } .hamburger::after { bottom: -8px; } /* HERO */ .hero { padding: 70px 0 90px; background: linear-gradient(180deg, var(--cream), var(--warm-white)); } .hero-grid { display: grid; grid-template-columns: 1fr 1fr; align-items: center; gap: 60px; } .hero-content { text-align: right; } .hero-tag { margin: 0 0 12px; color: var(--brown-light); font-size: .9rem; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; } .hero-sub { margin: 20px 0 32px; color: var(--brown-medium); font-size: 1.25rem; line-height: 1.7; } .hero-visual { display: flex; align-items: center; justify-content: center; min-height: 330px; } /* CSS ART */ .comfort-plate { position: relative; width: 300px; height: 300px; } .plate { position: absolute; inset: 20px; border-radius: 50%; background: radial-gradient(ellipse at 30% 30%, #f5ede4, #dccfc4); box-shadow: inset 0 -20px 40px rgba(61,42,30,.15), 0 20px 60px rgba(61,42,30,.2); } .plate::after { content: ""; position: absolute; width: 68%; height: 68%; left: 16%; top: 16%; border-radius: 50%; background: radial-gradient(ellipse at 40% 40%, #f0d5b0, #d4a373); box-shadow: inset 0 -10px 20px rgba(61,42,30,.1); } .steam { position: absolute; width: 12px; height: 42px; border-radius: 50%; background: rgba(255,255,255,.34); filter: blur(4px); animation: steam-rise 3s ease-in-out infinite; } .steam-1 { left: 37%; top: 10%; } .steam-2 { left: 50%; top: 3%; height: 52px; animation-delay: 1s; } .steam-3 { left: 63%; top: 10%; animation-delay: 2s; } @keyframes steam-rise { 0% { transform: translateY(0) scale(1); opacity: .6; } 50% { transform: translateY(-30px) scale(1.4); opacity: .2; } 100% { transform: translateY(-60px) scale(.8); opacity: 0; } } /* FINDER */ .finder { padding: 90px 0; background: var(--warm-white); } .finder > .container > h2, .finder-sub { text-align: center; } .finder-sub { color: var(--brown-medium); font-size: 1.1rem; margin: 0 0 48px; } .finder-step { border: 0; padding: 0; margin: 0 0 34px; } .finder-step-label { display: block; text-align: center; margin-bottom: 20px; color: var(--brown-dark); font-size: 1.2rem; font-weight: 700; } .mood-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 18px; max-width: 1000px; margin: auto; } .mood-card, .pref-card, .time-card { border: 2px solid transparent; background: var(--white); color: var(--brown-dark); cursor: pointer; box-shadow: var(--shadow); transition: transform var(--transition), border-color var(--transition), background var(--transition), color var(--transition), box-shadow var(--transition); } .mood-card { min-height: 175px; padding: 24px 16px; border-radius: var(--radius); text-align: center; } .mood-card:hover, .pref-card:hover, .time-card:hover { transform: translateY(-3px); border-color: var(--gold); box-shadow: var(--shadow-hover); } .mood-card.selected, .pref-card.selected, .time-card.selected { background: var(--brown-dark); color: var(--cream); border-color: var(--brown-dark); } .mood-icon { display: block; font-size: 2.7rem; margin-bottom: 8px; } .mood-label { display: block; font-weight: 800; font-size: 1.05rem; } .mood-desc { display: block; margin-top: 5px; color: var(--brown-light); font-size: .84rem; } .mood-card.selected .mood-desc { color: rgba(255,255,255,.72); } .pref-grid, .time-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 14px; max-width: 850px; margin: auto; } .pref-card, .time-card { padding: 16px 12px; border-radius: var(--radius-sm); font-weight: 700; min-height: 54px; } /* RECOMMENDATION */ .recommendation { max-width: 760px; margin: 48px auto 0; animation: fade-up .5s ease; } @keyframes fade-up { from { opacity: 0; transform: translateY(25px); } to { opacity: 1; transform: translateY(0); } } .rec-card { background: var(--white); border-radius: var(--radius); padding: 38px; box-shadow: var(--shadow-hover); display: grid; grid-template-columns: 130px 1fr; gap: 30px; align-items: center; } .rec-visual { display: flex; align-items: center; justify-content: center; } .rec-emoji { font-size: 5rem; line-height: 1; } .rec-label { margin: 0; color: var(--brown-light); font-size: .8rem; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; } .rec-content h3 { margin: 6px 0; } .rec-desc { color: var(--brown-medium); margin: 8px 0 16px; } .rec-meta { display: flex; flex-wrap: wrap; gap: 10px 16px; color: var(--brown-light); font-size: .9rem; margin-bottom: 20px; } .rec-actions { display: flex; flex-wrap: wrap; gap: 12px; } /* MODAL */ .modal[hidden], .recommendation[hidden], .finder-step[hidden] { display: none !important; } .modal { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 24px; } .modal-overlay { position: absolute; inset: 0; background: rgba(61,42,30,.65); backdrop-filter: blur(4px); } .modal-content { position: relative; z-index: 1; width: min(600px, 100%); max-height: 90vh; overflow-y: auto; padding: 40px; border-radius: var(--radius); background: var(--white); box-shadow: 0 40px 80px rgba(0,0,0,.3); animation: fade-up .3s ease; } .modal-close { position: absolute; top: 14px; left: 14px; width: 42px; height: 42px; border: 0; border-radius: 50%; background: transparent; color: var(--brown-light); cursor: pointer; font-size: 1.4rem; } .modal-close:hover { background: var(--cream); color: var(--brown-dark); } .modal-dish-emoji { text-align: center; font-size: 4rem; margin-bottom: 8px; } .modal-body h3 { margin-bottom: 8px; } .modal-description { color: var(--brown-medium); margin-bottom: 18px; } .dish-detail { margin: 14px 0; } .dish-detail strong { color: var(--brown-medium); } .modal-body ul { list-style: none; padding: 0; margin: 8px 0 0; } .modal-body li { padding: 6px 0; border-bottom: 1px solid var(--border); } /* MENU */ .menu { padding: 90px 0; background: var(--cream); } .menu > .container > h2, .menu-sub { text-align: center; } .menu-sub { color: var(--brown-medium); margin: 0 0 40px; } .filter-bar { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 40px; } .filter-btn { padding: 10px 22px; border: 2px solid transparent; border-radius: 999px; background: var(--white); color: var(--brown-dark); cursor: pointer; font-size: .9rem; font-weight: 700; transition: var(--transition); } .filter-btn:hover { border-color: var(--gold); } .filter-btn.active, .filter-btn[aria-pressed="true"] { background: var(--brown-dark); color: var(--cream); border-color: var(--brown-dark); } .dish-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 22px; } .dish-card { background: var(--white); border-radius: var(--radius); padding: 24px; text-align: center; box-shadow: var(--shadow); transition: var(--transition); } .dish-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); } .dish-emoji { display: block; font-size: 3.5rem; line-height: 1; margin-bottom: 10px; } .dish-name { font-weight: 800; font-size: 1.08rem; } .dish-category { margin-top: 3px; color: var(--brown-light); font-size: .78rem; font-weight: 700; } .dish-tags { display: flex; justify-content: center; flex-wrap: wrap; gap: 6px; margin: 10px 0 14px; } .dish-tag { padding: 3px 10px; border-radius: 999px; background: var(--cream); color: var(--brown-medium); font-size: .7rem; } .dish-card .btn { width: 100%; min-height: 42px; padding: 9px 16px; font-size: .86rem; } /* STORY / CTA / FOOTER */ .story { padding: 100px 0; background: var(--brown-dark); color: var(--cream); } .story-content { max-width: 760px; margin: auto; text-align: center; } .story-tag { color: var(--gold); font-size: .8rem; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; } .story .highlight { color: var(--gold); } .story-text { color: rgba(255,255,255,.8); font-size: 1.12rem; line-height: 1.9; margin-top: 20px; } .cta { padding: 85px 0; text-align: center; background: linear-gradient(180deg, var(--cream), var(--warm-white)); } .cta-sub { color: var(--brown-medium); font-size: 1.15rem; margin: 0 0 30px; } footer { padding: 60px 0 28px; background: var(--brown-dark); color: rgba(255,255,255,.72); border-top: 2px solid rgba(255,255,255,.05); } .footer-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-bottom: 40px; } .footer-brand .logo { color: var(--cream); } .footer-tagline { color: var(--gold-light); font-style: italic; margin: 8px 0 0; } .footer-desc { font-size: .9rem; opacity: .7; margin: 4px 0 0; } .footer-credit { text-align: left; line-height: 1.8; } .footer-credit strong { color: var(--cream); } .footer-credit a { color: var(--gold); text-decoration: none; } .footer-credit a:hover, .footer-credit a:focus-visible { color: var(--gold-light); text-decoration: underline; } .footer-bottom { border-top: 1px solid rgba(255,255,255,.08); padding-top: 24px; text-align: center; font-size: .85rem; opacity: .55; } /* RESPONSIVE */ @media (max-width: 1050px) { .mood-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } .dish-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } .hero-grid { grid-template-columns: 1fr; text-align: center; } .hero-content { text-align: center; } .hero-visual { order: -1; } } @media (max-width: 768px) { .container { width: min(100% - 32px, 1200px); } .nav-toggle { display: block; } nav ul { display: none; position: absolute; top: calc(100% + 12px); left: 0; min-width: 210px; padding: 18px; flex-direction: column; align-items: stretch; gap: 12px; background: var(--white); border-radius: var(--radius-sm); box-shadow: var(--shadow-hover); } nav ul.open { display: flex; } .mood-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } .pref-grid, .time-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } .rec-card { grid-template-columns: 1fr; text-align: center; } .rec-actions { justify-content: center; } .footer-grid { grid-template-columns: 1fr; text-align: center; } .footer-credit { text-align: center; } } @media (max-width: 480px) { .hero { padding: 45px 0 65px; } .comfort-plate { width: 220px; height: 220px; } .plate { inset: 15px; } .mood-grid { grid-template-columns: 1fr; } .pref-grid, .time-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } .dish-grid { grid-template-columns: 1fr; } .rec-card, .modal-content { padding: 24px; } .btn { width: 100%; } .rec-actions { flex-direction: column; } .modal-content { margin: 8px; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } .steam { display: none; } .recommendation, .modal-content { animation: none; } }
🍲
Comfort Table
<nav aria-label="القائمة الرئيسية"> <button class="nav-toggle" id="nav-toggle" type="button" aria-expanded="false" aria-controls="nav-menu"> <span class="sr-only">فتح القائمة</span> <span class="hamburger" aria-hidden="true"></span> </button> <ul id="nav-menu"> <li><a href="#finder" aria-current="page">ابحث عن الراحة</a></li> <li><a href="#menu">القائمة</a></li> <li><a href="#story">القصة</a></li> </ul> </nav> </div>🍲 Comfort Table
طعم يشبه
المنزلأخبرنا بمشاعرك.
ابحث عن راحتك ←
سنجد لك طعامك المريح.<div class="hero-visual" aria-hidden="true"> <div class="comfort-plate"> <div class="plate"></div> <div class="steam steam-1"></div> <div class="steam steam-2"></div> <div class="steam steam-3"></div> </div> </div> </div> </section> <section class="finder" id="finder" aria-labelledby="finder-title"> <div class="container"> <h2 id="finder-title">ما نوع الراحة التي تبحث عنها؟</h2> <p class="finder-sub">اختر شعورك، وسنوصي بالطبق المناسب لك.</p> <div id="rec-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></div> <fieldset class="finder-step"> <legend class="sr-only">الخطوة 1: اختر شعورك</legend> <div class="mood-grid" id="mood-grid"> <button class="mood-card" data-mood="home" type="button" aria-pressed="false"> <span class="mood-icon" aria-hidden="true">🏠</span> <span class="mood-label">المنزل</span> <span class="mood-desc">أشتاق إلى طعم المنزل.</span> </button> <button class="mood-card" data-mood="nostalgia" type="button" aria-pressed="false"> <span class="mood-icon" aria-hidden="true">❤️</span> <span class="mood-label">الحنين</span> <span class="mood-desc">أريد شيئًا يعيد الذكريات.</span> </button> <button class="mood-card" data-mood="cozy" type="button" aria-pressed="false"> <span class="mood-icon" aria-hidden="true">😌</span> <span class="mood-label">الدفء</span> <span class="mood-desc">أريد شيئًا دافئًا ومريحًا.</span> </button> <button class="mood-card" data-mood="hearty" type="button" aria-pressed="false"> <span class="mood-icon" aria-hidden="true">🔥</span> <span class="mood-label">المشبع</span> <span class="mood-desc">أريد وجبة تشبعني.</span> </button> <button class="mood-card" data-mood="quick" type="button" aria-pressed="false"> <span class="mood-icon" aria-hidden="true">⚡</span> <span class="mood-label">السريع</span> <span class="mood-desc">راحة سريعة.</span> </button> </div> </fieldset> <fieldset class="finder-step" id="pref-step" hidden> <legend class="finder-step-label">ما الذي تفضله؟</legend> <div class="pref-grid" id="pref-grid"> <button class="pref-card" data-pref="rice" type="button" aria-pressed="false">🍚 أرز</button> <button class="pref-card" data-pref="meat" type="button" aria-pressed="false">🥩 لحم</button> <button class="pref-card" data-pref="chicken" type="button" aria-pressed="false">🍗 دجاج</button> <button class="pref-card" data-pref="veg" type="button" aria-pressed="false">🥗 نباتي</button> <button class="pref-card" data-pref="sweet" type="button" aria-pressed="false">🍯 حلو</button> <button class="pref-card" data-pref="any" type="button" aria-pressed="false">✨ أي شيء</button> </div> </fieldset> <fieldset class="finder-step" id="time-step" hidden> <legend class="finder-step-label">كم لديك من الوقت؟</legend> <div class="time-grid" id="time-grid"> <button class="time-card" data-time="10" type="button" aria-pressed="false">⏱ 10 دقائق</button> <button class="time-card" data-time="30" type="button" aria-pressed="false">⏱ 30 دقيقة</button> <button class="time-card" data-time="any" type="button" aria-pressed="false">⏱ لا يهم</button> </div> </fieldset> <div class="recommendation" id="recommendation" hidden> <div class="rec-card" id="rec-card" tabindex="-1"> <div class="rec-visual" aria-hidden="true"> <span class="rec-emoji" id="rec-emoji">🥘</span> </div> <div class="rec-content"> <p class="rec-label">❤️ وجبتك المثالية</p> <h3 id="rec-name">سلتة يمنية</h3> <p class="rec-desc" id="rec-desc"></p> <div class="rec-meta" id="rec-meta"></div> <div class="rec-actions"> <button class="btn btn-secondary" id="explore-dish" type="button">استكشف الطبق</button> <button class="btn btn-ghost" id="reset-finder" type="button">جرّب شعورًا آخر</button> </div> </div> </div> </div> </div> </section> <div class="modal" id="dish-modal" hidden role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-description"> <div class="modal-overlay" id="modal-overlay"></div> <div class="modal-content" role="document"> <button class="modal-close" id="modal-close" type="button" aria-label="إغلاق النافذة">✕</button> <div class="modal-body" id="modal-body"></div> </div> </div> <section class="menu" id="menu" aria-labelledby="menu-title"> <div class="container"> <h2 id="menu-title">استكشف قائمة الراحة</h2> <p class="menu-sub">أطباق يمنية وعربية محضرة بدفء وعناية.</p> <div class="filter-bar" role="group" aria-label="تصفية الأطباق"> <button class="filter-btn active" data-filter="all" aria-pressed="true" type="button">الكل</button> <button class="filter-btn" data-filter="bowl" aria-pressed="false" type="button">أطباق رئيسية</button> <button class="filter-btn" data-filter="bake" aria-pressed="false" type="button">مخبوزات</button> <button class="filter-btn" data-filter="sweet" aria-pressed="false" type="button">حلويات</button> <button class="filter-btn" data-filter="quick" aria-pressed="false" type="button">سريع (≤ 30 د)</button> </div> <div class="dish-grid" id="dish-grid" role="list" aria-live="polite"></div> </div> </section> <section class="story" id="story" aria-labelledby="story-title"> <div class="container"> <div class="story-content"> <p class="story-tag">القصة</p> <h2 id="story-title">بعض الوجبات تُطعمك.<br><span class="highlight">وبعضها يتذكرك.</span></h2> <p class="story-text">الطعام المريح ليس مجرد مكونات. إنه اللحظات التي تحملها — مطبخ الجدة، بعد ظهر ممطر، وجبة تشاركها مع من تحب.</p> <p class="story-text">في Comfort Table، نؤمن أن كل طبق يحكي قصة. أخبرنا بمشاعرك، وسنجد لك الطبق الذي يتحدث إليك.</p> </div> </div> </section> <section class="cta" aria-labelledby="cta-title"> <div class="container"> <h2 id="cta-title">وجبتك المريحة التالية تنتظرك.</h2> <p class="cta-sub">ابحث عن الطبق الذي يشعرك وكأنك في المنزل.</p> <a class="btn btn-primary" href="#finder">ابحث عن راحتك ←</a> </div> </section>
🍲
Comfort Table
طعم يشبه المنزل.
مشروع واجهة وتجربة طعام رقمية.
<div class="footer-credit"> <p><strong>تصميم وتطوير<br>جميل الحطامي</strong></p> <p>شركة جميل الحطامي للتسويق والمبيعات للتسوق الإلكتروني</p> <p>تم بناء هذا المشروع لتحدي DEV Community — Comfort Food Edition.</p> <p> <a href="https://github.com/2gmil991-cloud" target="_blank" rel="noopener noreferrer">GitHub</a> · <a href="https://dancing-paletas-8f7e29.netlify.app/" target="_blank" rel="noopener noreferrer">الموقع المنشور</a> </p> </div> </div> <div class="footer-bottom"> <p>© 2026 Comfort Table. جميع الحقوق محفوظة.</p> </div> </div><br> "use strict";</p> <div class="highlight"><pre class="highlight plaintext"><code>/* ============================================================ DATA ============================================================ */ const dishes = [ { id: "saltah", name: "سلتة يمنية", emoji: "🥘", category: "bowl", moods: ["home", "hearty", "cozy"], preferences: ["meat", "chicken"], time: 30, description: "وجبة يمنية أصيلة تُقدم في إناء فخاري مع اللحم والخضروات والبهارات العطرية.", ingredients: ["لحم", "طماطم", "فلفل", "ثوم", "بهارات يمنية", "خبز ملوج"], tags: ["يمني", "مشبع", "دافئ"], comfortLevel: 5 }, { id: "mandi", name: "مندي", emoji: "🍚", category: "bowl", moods: ["home", "hearty", "nostalgia"], preferences: ["rice", "meat", "chicken"], time: 60, description: "أرز مندي بلحم الضأن أو الدجاج، مطبوخ على الطريقة اليمنية الأصيلة مع التوابل والزعفران.", ingredients: ["أرز بسمتي", "لحم ضأن أو دجاج", "بهارات", "زعفران", "تمر", "مكسرات"], tags: ["يمني", "مشبع", "تقليدي"], comfortLevel: 5 }, { id: "shorba", name: "شوربة دافئة", emoji: "🍲", category: "bowl", moods: ["cozy", "home", "quick"], preferences: ["veg", "chicken"], time: 20, description: "شوربة غنية بالخضروات والدجاج، تمنحك الدفء في كل ملعقة.", ingredients: ["دجاج", "خضروات مشكلة", "نودلز", "بهارات", "ليمون"], tags: ["دافئ", "مريح", "سريع"], comfortLevel: 4 }, { id: "kabsa", name: "كبسة", emoji: "🍛", category: "bowl", moods: ["home", "hearty"], preferences: ["rice", "meat", "chicken"], time: 45, description: "طبق خليجي شهير، أرز مطهو مع اللحم أو الدجاج والتوابل العطرية.", ingredients: ["أرز", "لحم أو دجاج", "بصل", "طماطم", "بهارات كبسة", "مكسرات"], tags: ["خليجي", "مشبع", "منزلي"], comfortLevel: 4 }, { id: "malooga", name: "ملوج بالعسل", emoji: "🫓", category: "bake", moods: ["nostalgia", "cozy"], preferences: ["sweet"], time: 30, description: "خبز يمني تقليدي مع عسل وزبدة؛ دفء العائلة في كل قضمة.", ingredients: ["طحين", "عسل", "زبدة", "خميرة", "هيل"], tags: ["يمني", "حلو", "تقليدي"], comfortLevel: 5 }, { id: "bint-al-sahn", name: "بنت الصحن", emoji: "🍯", category: "sweet", moods: ["nostalgia", "home", "cozy"], preferences: ["sweet"], time: 40, description: "حلوى يمنية شهيرة من طبقات العجين والعسل والزبدة، تقدم في المناسبات.", ingredients: ["طحين", "عسل", "زبدة", "زعفران", "هيل"], tags: ["يمني", "حلو", "احتفالي"], comfortLevel: 5 }, { id: "fahsa", name: "فحسة", emoji: "🥩", category: "bowl", moods: ["hearty", "home"], preferences: ["meat"], time: 50, description: "لحم مطهو مع التوابل اليمنية والفلفل الحار، يقدم مع خبز ملوج.", ingredients: ["لحم", "فلفل حار", "طماطم", "ثوم", "بهارات", "خبز"], tags: ["يمني", "مشبع", "دافئ"], comfortLevel: 5 }, { id: "zabadi", name: "زبادي بالعسل", emoji: "🥛", category: "sweet", moods: ["quick", "cozy"], preferences: ["sweet"], time: 5, description: "زبادي طازج مع عسل ومكسرات؛ راحة سريعة وبسيطة.", ingredients: ["زبادي", "عسل", "جوز", "فستق"], tags: ["سريع", "منعش", "خفيف"], comfortLevel: 3 } ]; const categoryNames = { bowl: "أطباق رئيسية", bake: "مخبوزات", sweet: "حلويات" }; const state = { mood: null, preference: null, time: null, currentDish: null, lastFocusedElement: null, activeFilter: "all" }; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; const moodGrid = $("#mood-grid"); const prefGrid = $("#pref-grid"); const timeGrid = $("#time-grid"); const prefStep = $("#pref-step"); const timeStep = $("#time-step"); const recommendation = $("#recommendation"); const recCard = $("#rec-card"); const recEmoji = $("#rec-emoji"); const recName = $("#rec-name"); const recDesc = $("#rec-desc"); const recMeta = $("#rec-meta"); const recAnnouncer = $("#rec-announcer"); const exploreBtn = $("#explore-dish"); const resetBtn = $("#reset-finder"); const dishGrid = $("#dish-grid"); const modal = $("#dish-modal"); const modalBody = $("#modal-body"); const modalClose = $("#modal-close"); const modalOverlay = $("#modal-overlay"); const navToggle = $("#nav-toggle"); const navMenu = $("#nav-menu"); const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; function scrollToElement(element, block = "center") { element.scrollIntoView({ behavior: prefersReducedMotion ? "auto" : "smooth", block }); } /* ============================================================ WEIGHTED SCORING — 40% + 30% + 20% + 10% = 100% Mood = 40 Preference = 30 Time = 20 Comfort = 10 ============================================================ */ function scoreTime(dishTime, selectedTime) { if (!selectedTime || selectedTime === "any") return 20; const maxTime = Number(selectedTime); if (dishTime <= maxTime) return 20; const difference = dishTime - maxTime; if (difference <= 15) return 10; return 0; } function calculateMatchScore(dish, mood, preference, time) { let score = 0; if (dish.moods.includes(mood)) score += 40; if (!preference || preference === "any") { score += 30; } else if (dish.preferences.includes(preference)) { score += 30; } score += scoreTime(dish.time, time); score += dish.comfortLevel * 2; return Math.min(100, Math.round(score)); } function getRecommendation(mood, preference, time) { if (!mood) return null; const scored = dishes.map(dish => ({ ...dish, matchScore: calculateMatchScore(dish, mood, preference, time) })); scored.sort((a, b) => { if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore; if (a.time !== b.time) return a.time - b.time; return b.comfortLevel - a.comfortLevel; }); return scored[0] || null; } function showRecommendation(dish) { state.currentDish = dish; recEmoji.textContent = dish.emoji; recName.textContent = dish.name; recDesc.textContent = dish.description; recMeta.innerHTML = [ `<span>⏱ ${dish.time} دقيقة</span>`, `<span>🏷️ ${dish.tags.join(" · ")}</span>`, `<span>⭐ تطابق ${dish.matchScore}%</span>` ].join(""); recommendation.hidden = false; recAnnouncer.textContent = `تم العثور على طبقك المثالي: ${dish.name}، بنسبة تطابق ${dish.matchScore} بالمئة.`; scrollToElement(recommendation); window.setTimeout(() => { recCard.focus({ preventScroll: true }); }, prefersReducedMotion ? 0 : 250); } /* ============================================================ SELECTION HELPERS ============================================================ */ function setPressed(container, selector, selected) { $$(selector, container).forEach(button => { const active = button === selected; button.classList.toggle("selected", active); button.setAttribute("aria-pressed", String(active)); }); } function resetFinder() { state.mood = null; state.preference = null; state.time = null; state.currentDish = null; $$(".mood-card, .pref-card, .time-card").forEach(button => { button.classList.remove("selected"); button.setAttribute("aria-pressed", "false"); }); prefStep.hidden = true; timeStep.hidden = true; recommendation.hidden = true; recAnnouncer.textContent = "تمت إعادة تعيين البحث. اختر شعورك للبدء."; scrollToElement(moodGrid, "start"); window.setTimeout(() => { const firstMood = $(".mood-card", moodGrid); if (firstMood) firstMood.focus({ preventScroll: true }); }, prefersReducedMotion ? 0 : 250); } /* ============================================================ FINDER EVENTS ============================================================ */ moodGrid.addEventListener("click", event => { const button = event.target.closest(".mood-card"); if (!button) return; state.mood = button.dataset.mood; state.preference = null; state.time = null; state.currentDish = null; setPressed(moodGrid, ".mood-card", button); $$(".pref-card", prefGrid).forEach(item => { item.classList.remove("selected"); item.setAttribute("aria-pressed", "false"); }); $$(".time-card", timeGrid).forEach(item => { item.classList.remove("selected"); item.setAttribute("aria-pressed", "false"); }); prefStep.hidden = false; timeStep.hidden = true; recommendation.hidden = true; scrollToElement(prefStep); window.setTimeout(() => { const firstPreference = $(".pref-card", prefGrid); if (firstPreference) firstPreference.focus({ preventScroll: true }); }, prefersReducedMotion ? 0 : 250); }); prefGrid.addEventListener("click", event => { const button = event.target.closest(".pref-card"); if (!button) return; state.preference = button.dataset.pref; state.time = null; setPressed(prefGrid, ".pref-card", button); $$(".time-card", timeGrid).forEach(item => { item.classList.remove("selected"); item.setAttribute("aria-pressed", "false"); }); timeStep.hidden = false; recommendation.hidden = true; scrollToElement(timeStep); window.setTimeout(() => { const firstTime = $(".time-card", timeGrid); if (firstTime) firstTime.focus({ preventScroll: true }); }, prefersReducedMotion ? 0 : 250); }); timeGrid.addEventListener("click", event => { const button = event.target.closest(".time-card"); if (!button) return; state.time = button.dataset.time; setPressed(timeGrid, ".time-card", button); const dish = getRecommendation( state.mood, state.preference, state.time ); if (dish) showRecommendation(dish); }); resetBtn.addEventListener("click", resetFinder); /* ============================================================ MODAL — FOCUS MANAGEMENT + ESC + CLICK OUTSIDE ============================================================ */ function getFocusableInModal() { return $$( 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', modal ); } function trapFocus(event) { if (modal.hidden) return; if (event.key === "Escape") { event.preventDefault(); closeModal(); return; } if (event.key !== "Tab") return; const focusable = getFocusableInModal(); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } function openModal(dish, returnFocusElement = document.activeElement) { if (!dish) return; state.lastFocusedElement = returnFocusElement || null; modalBody.innerHTML = ` <div class="modal-dish-emoji" aria-hidden="true">${dish.emoji}</div> <h3 id="modal-title">${dish.name}</h3> <p class="modal-description" id="modal-description">${dish.description}</p> <div class="dish-detail"> <strong>⏱ وقت التحضير:</strong> ${dish.time} دقيقة </div> <div class="dish-detail"> <strong>🏷️ التصنيفات:</strong> ${dish.tags.join(" · ")} </div> <div class="dish-detail"> <strong>🥄 المكونات:</strong> <ul> ${dish.ingredients.map(item => `<li>${item}</li>`).join("")} </ul> </div> <div class="dish-detail"> <strong>❤️ مستوى الراحة:</strong> ${"⭐".repeat(dish.comfortLevel)}${"☆".repeat(5 - dish.comfortLevel)} </div> `; modal.hidden = false; document.body.classList.add("modal-open"); window.setTimeout(() => { modalClose.focus(); }, prefersReducedMotion ? 0 : 50); } function closeModal() { if (modal.hidden) return; modal.hidden = true; document.body.classList.remove("modal-open"); const returnTarget = state.lastFocusedElement; state.lastFocusedElement = null; if (returnTarget && typeof returnTarget.focus === "function") { window.setTimeout(() => returnTarget.focus({ preventScroll: true }), 0); } } modal.addEventListener("keydown", trapFocus); modalClose.addEventListener("click", closeModal); modalOverlay.addEventListener("click", closeModal); exploreBtn.addEventListener("click", () => { if (state.currentDish) { openModal(state.currentDish, exploreBtn); } }); /* ============================================================ MENU RENDERING ============================================================ */ function filterData(filter) { if (filter === "all") return dishes; if (filter === "quick") return dishes.filter(dish => dish.time <= 30); return dishes.filter(dish => dish.category === filter); } function renderDishes(filter = "all") { state.activeFilter = filter; const filtered = filterData(filter); if (!filtered.length) { dishGrid.innerHTML = ` <p style="grid-column:1/-1;text-align:center;color:var(--brown-light);"> لا توجد أطباق في هذه الفئة. </p> `; return; } dishGrid.innerHTML = filtered.map(dish => ` <article class="dish-card" role="listitem"> <span class="dish-emoji" aria-hidden="true">${dish.emoji}</span> <div class="dish-name">${dish.name}</div> <div class="dish-category">${categoryNames[dish.category]}</div> <div class="dish-tags"> ${dish.tags.map(tag => `<span class="dish-tag">${tag}</span>`).join("")} </div> <button class="btn btn-secondary dish-explore" type="button" data-dish-id="${dish.id}" aria-label="استكشف تفاصيل ${dish.name}"> استكشف الطبق </button> </article> `).join(""); $$("button.dish-explore", dishGrid).forEach(button => { button.addEventListener("click", event => { const dish = dishes.find(item => item.id === event.currentTarget.dataset.dishId); if (dish) openModal(dish, event.currentTarget); }); }); } $$(".filter-btn").forEach(button => { button.addEventListener("click", () => { $$(".filter-btn").forEach(item => { item.classList.remove("active"); item.setAttribute("aria-pressed", "false"); }); button.classList.add("active"); button.setAttribute("aria-pressed", "true"); renderDishes(button.dataset.filter); }); }); /* ============================================================ NAVIGATION ============================================================ */ function setNavOpen(open) { navMenu.classList.toggle("open", open); navToggle.setAttribute("aria-expanded", String(open)); } navToggle.addEventListener("click", () => { setNavOpen(!navMenu.classList.contains("open")); }); $$("a", navMenu).forEach(link => { link.addEventListener("click", () => { setNavOpen(false); $$("a", navMenu).forEach(item => item.removeAttribute("aria-current")); if (link.getAttribute("href") !== "#main") { link.setAttribute("aria-current", "page"); } }); }); document.addEventListener("click", event => { if ( navMenu.classList.contains("open") && !navMenu.contains(event.target) && !navToggle.contains(event.target) ) { setNavOpen(false); } }); window.addEventListener("resize", () => { if (window.innerWidth > 768) setNavOpen(false); }); /* ============================================================ INIT ============================================================ */ renderDishes("all"); console.log("🍲 Comfort Table — V1.4 Final"); console.log("👨💻 Designed & Developed by Jamil Al-Hattami"); console.log("🏢 Jamil Al-Hattami Co. for Digital Marketing & E-Commerce"); console.log("📦 DEV Frontend Challenge — Comfort Food Edition"); console.log("🇾🇪 Yemeni & Arabic comfort food"); console.log("⚙️ Weighted Scoring: Mood 40% + Preference 30% + Time 20% + Comfort 10%"); console.log("🔍 Quick filter: time ≤ 30 minutes"); </code></pre></div> <p>
Frontend Challenge Perfect Landing Submission 🍲🥧
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)