Stable run with tailwind
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
@namespace MidrandBooks.Components.Layout
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using MidrandBooks.Models
|
||||
|
||||
<nav class="sticky top-0 z-40 border-b border-slate-100 bg-white/95 backdrop-blur-md px-4 sm:px-6 py-4">
|
||||
<div class="max-w-7xl mx-auto flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
|
||||
<!-- Brand Logo & South African Context -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div @onclick='() => SetTab("catalog")' class="flex items-center gap-2.5 cursor-pointer group">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-900 text-white shadow-sm group-hover:scale-105 transition-transform">
|
||||
<svg class="h-5 w-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-sans text-xl font-bold tracking-tight text-slate-800 uppercase">
|
||||
Midrand<span class="font-light text-slate-500">books</span><span class="text-xs font-normal lowercase text-violet-500">.co.za</span>
|
||||
</span>
|
||||
<p class="text-[9px] text-slate-400 tracking-wider uppercase font-mono">Est. 2026 • South Africa</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Mobile Action Icons -->
|
||||
<div class="flex items-center gap-2 md:hidden">
|
||||
<button
|
||||
@onclick="ToggleOfflineMode"
|
||||
class="p-2 rounded-lg border @(IsOfflineMode ? "border-amber-500/20 bg-amber-50/80 text-amber-600" : "border-emerald-500/20 bg-emerald-50/80 text-emerald-600")"
|
||||
title="@(IsOfflineMode ? "Offline Mode Active" : "Online Mode")">
|
||||
@if (IsOfflineMode)
|
||||
{
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-3.536 5 5 0 011.414-3.536M4.93 19.07a9 9 0 010-12.728M12 12v.01" />
|
||||
</svg>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
@onclick="HandleOpenCart"
|
||||
class="relative p-2 rounded-lg border border-slate-200 bg-slate-50 text-slate-700 hover:bg-slate-100">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
@if (CartCount > 0)
|
||||
{
|
||||
<span class="absolute -top-1 -right-1 flex h-4.5 w-4.5 items-center justify-center rounded-full bg-slate-900 text-[10px] font-bold text-white">
|
||||
@CartCount
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar Node and Desktop Navigation links -->
|
||||
<div class="flex-1 flex flex-col md:flex-row items-stretch md:items-center justify-end gap-4">
|
||||
|
||||
<!-- Dynamic Search input block -->
|
||||
<div class="relative flex-1 max-w-md" @onfocusout="HandleFocusOut">
|
||||
<span class="absolute inset-y-0 left-3 flex items-center text-slate-400">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search over 1,500 local publications..."
|
||||
value="@SearchQuery"
|
||||
@oninput="HandleSearchInput"
|
||||
@onfocus="() => ShowResults = true"
|
||||
class="w-full rounded-xl border border-slate-200 bg-slate-50 pl-9 pr-4 py-2 text-xs text-slate-800 placeholder-slate-400 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15 focus:border-violet-500 transition-all font-sans"
|
||||
/>
|
||||
|
||||
<!-- Floating Search Results Drawer -->
|
||||
@if (ShowResults && FilteredBooks().Any())
|
||||
{
|
||||
<div class="absolute left-0 right-0 mt-2 rounded-2xl border border-slate-200/80 bg-white p-2 shadow-xl z-50">
|
||||
<div class="px-3 py-1.5 border-b border-slate-50">
|
||||
<span class="text-[9px] font-mono font-bold tracking-wider text-slate-400 uppercase">Interactive Catalog Search Results</span>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-50 max-h-[300px] overflow-y-auto">
|
||||
@foreach (var book in FilteredBooks())
|
||||
{
|
||||
<div @onclick="() => ClickSearchResult(book)" class="p-2.5 hover:bg-slate-50 rounded-xl flex items-center gap-3 cursor-pointer transition-colors text-left">
|
||||
<div class="h-10 w-7 rounded bg-slate-100 overflow-hidden flex-shrink-0 shadow-sm border border-slate-100">
|
||||
@if (!string.IsNullOrEmpty(book.CoverUrl))
|
||||
{
|
||||
<img src="@book.CoverUrl" alt="@book.Title" class="h-full w-full object-cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="h-full w-full bg-gradient-to-br @book.CoverColor flex items-center justify-center">
|
||||
<span class="text-[6px] text-white/50">BOOK</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="block text-xs font-bold text-slate-800 truncate">@book.Title</span>
|
||||
<span class="block text-[10px] text-slate-400 truncate">By @book.Author • <span class="font-mono text-[9px] text-violet-500 font-bold uppercase">@book.Category</span></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Central Platform Tabs navigation -->
|
||||
<div class="flex items-center justify-around md:justify-end gap-1 border-t md:border-t-0 border-slate-100 pt-3 md:pt-0">
|
||||
<button
|
||||
@onclick='() => SetTab("catalog")'
|
||||
class="px-4 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-2 cursor-pointer @(ActiveTab == "catalog" ? "bg-slate-900 text-white shadow-sm" : "text-slate-500 hover:text-slate-800 hover:bg-slate-50")">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
Browse Store
|
||||
</button>
|
||||
|
||||
<button
|
||||
@onclick='() => SetTab("library")'
|
||||
class="px-4 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-2 cursor-pointer @(ActiveTab == "library" ? "bg-slate-900 text-white shadow-sm" : "text-slate-500 hover:text-slate-800 hover:bg-slate-50")">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z" />
|
||||
</svg>
|
||||
My Library
|
||||
</button>
|
||||
|
||||
<button
|
||||
@onclick='() => SetTab("author")'
|
||||
class="px-4 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-2 cursor-pointer @(ActiveTab == "author" ? "bg-slate-900 text-white shadow-sm" : "text-slate-500 hover:text-slate-800 hover:bg-slate-50")">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.907c.961 0 1.364 1.241.588 1.81l-3.974 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.974-2.888c-.776-.569-.373-1.81.588-1.81h4.906a1 1 0 00.95-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
Author Studio
|
||||
</button>
|
||||
|
||||
<!-- Desktop Quick Actions -->
|
||||
<div class="hidden md:flex items-center gap-2 border-l border-slate-100 pl-4 ml-1">
|
||||
<button
|
||||
@onclick="ToggleOfflineMode"
|
||||
class="p-2 rounded-xl border transition-colors cursor-pointer @(IsOfflineMode ? "border-amber-500/20 bg-amber-50/80 text-amber-600 hover:bg-amber-100/80" : "border-emerald-500/20 bg-emerald-50/80 text-emerald-600 hover:bg-emerald-100/80")"
|
||||
title="@(IsOfflineMode ? "Offline Mode Active" : "Online Mode Live")">
|
||||
@if (IsOfflineMode)
|
||||
{
|
||||
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-3.536 5 5 0 011.414-3.536M4.93 19.07a9 9 0 010-12.728M12 12v.01" />
|
||||
</svg>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
@onclick="HandleOpenCart"
|
||||
class="relative p-2 rounded-xl border border-slate-200 bg-slate-50 text-slate-700 hover:bg-slate-100 transition-all cursor-pointer">
|
||||
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
@if (CartCount > 0)
|
||||
{
|
||||
<span class="absolute -top-1.5 -right-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-slate-900 text-[10px] font-bold text-white shadow-sm border border-white">
|
||||
@CartCount
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
[Parameter] public string ActiveTab { get; set; } = "catalog";
|
||||
[Parameter] public EventCallback<string> OnActiveTabChanged { get; set; }
|
||||
[Parameter] public EventCallback OnOpenCart { get; set; }
|
||||
[Parameter] public EventCallback<BookModel> OnSelectBook { get; set; }
|
||||
[Parameter] public List<BookModel> Books { get; set; } = new();
|
||||
[Parameter] public int CartCount { get; set; }
|
||||
[Parameter] public bool IsOfflineMode { get; set; }
|
||||
[Parameter] public EventCallback<bool> OnOfflineModeToggle { get; set; }
|
||||
|
||||
private string SearchQuery { get; set; } = string.Empty;
|
||||
private bool ShowResults { get; set; } = false;
|
||||
|
||||
private void SetTab(string tab)
|
||||
{
|
||||
OnActiveTabChanged.InvokeAsync(tab);
|
||||
}
|
||||
|
||||
private void ToggleOfflineMode()
|
||||
{
|
||||
OnOfflineModeToggle.InvokeAsync(!IsOfflineMode);
|
||||
}
|
||||
|
||||
private void HandleOpenCart()
|
||||
{
|
||||
OnOpenCart.InvokeAsync();
|
||||
}
|
||||
|
||||
private void HandleSearchInput(ChangeEventArgs e)
|
||||
{
|
||||
SearchQuery = e.Value?.ToString() ?? string.Empty;
|
||||
ShowResults = !string.IsNullOrWhiteSpace(SearchQuery);
|
||||
}
|
||||
|
||||
private IEnumerable<BookModel> FilteredBooks()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SearchQuery)) return Enumerable.Empty<BookModel>();
|
||||
|
||||
var query = SearchQuery.Trim().ToLower();
|
||||
return Books
|
||||
.Where(b => b.Status == "published" &&
|
||||
(b.Title.ToLower().Contains(query) ||
|
||||
b.Author.ToLower().Contains(query) ||
|
||||
b.Category.ToLower().Contains(query)))
|
||||
.Take(5);
|
||||
}
|
||||
|
||||
private void ClickSearchResult(BookModel book)
|
||||
{
|
||||
OnSelectBook.InvokeAsync(book);
|
||||
SearchQuery = string.Empty;
|
||||
ShowResults = false;
|
||||
}
|
||||
|
||||
private async Task HandleFocusOut()
|
||||
{
|
||||
// Simple delay to allow click events on items to execute before drawer closes
|
||||
await Task.Delay(200);
|
||||
ShowResults = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user