Skip to content

Commit

Permalink
feat: add better UX when js loading at projects page
Browse files Browse the repository at this point in the history
  • Loading branch information
canyugs committed Apr 30, 2024
1 parent 42afd83 commit c8daffb
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 17 deletions.
46 changes: 45 additions & 1 deletion src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}
}

:root{
:root {
view-transition-name: none;
}

Expand Down Expand Up @@ -122,3 +122,47 @@ body {
header {
position: relative;
}

#spinner {
display: none;
}

.spinner {
width: 25px;
height: 25px;
border: 5px solid white;
border-top: 5px solid #d3420e;
border-radius: 50%;
animation: spin 1s linear infinite;
}

.spinner-more {
width: 25px;
height: 25px;
border: 5px solid #d3420e;
border-top: 5px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}

.spinner-gray {
width: 25px;
height: 25px;
border: 5px solid white;
border-top: 5px solid rgb(75 85 99);
border-radius: 50%;
animation: spin 1s linear infinite;
}

input:checked ~ #spinner {
display: block;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
25 changes: 24 additions & 1 deletion src/routes/projects/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type FilterProps = {
categoryName: string;
filterOptions: string[];
store?: any;
spinnerController?: any;
};

export default component$<FilterProps>((props) => {
Expand All @@ -13,11 +14,23 @@ export default component$<FilterProps>((props) => {
filters: new Set(),
});

useTask$(() => {
// restore checkbox state from store
if (props.store[props.filterName]) {
state.filters = new Set(props.store[props.filterName]);
filterSize.value = state.filters.size;
}
});

useTask$(({ track }) => {
track(() => filterSize.value);
props.store[props.filterName] = Array.from(state.filters);
});

const isOptionChecked = (option: string, filters: string[]) => {
return filters.indexOf(option) !== -1;
};

const handleFilterChange = $((e: Event) => {
const target = e.target as HTMLElement;

Expand Down Expand Up @@ -80,7 +93,7 @@ export default component$<FilterProps>((props) => {
return (
<div
key={option}
class="flex cursor-pointer items-center gap-4"
class="relative flex cursor-pointer items-center gap-4"
onClick$={handleFilterChange}
>
<input
Expand All @@ -92,10 +105,20 @@ export default component$<FilterProps>((props) => {
type="checkbox"
value={option}
onChange$={handleFilterChange}
checked={isOptionChecked(option, props.store[props.filterName])}
/>
<label for={option} class="pointer-events-none">
{option}
</label>
<div
id="spinner"
class={[
"pointer-events-none absolute right-2",
props.spinnerController === "hidden" ? "!hidden" : "block",
]}
>
<div class="spinner" />
</div>
</div>
);
})}
Expand Down
69 changes: 65 additions & 4 deletions src/routes/projects/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useSignal,
useStore,
useComputed$,
useOnDocument,
} from "@builder.io/qwik";
import Section from "~/components/section";
import { RepoBlock } from "~/routes/projects/repo-block";
Expand Down Expand Up @@ -37,6 +38,9 @@ export default component$(() => {
projects: localProjects,
});

const projectListRef = useSignal<Element>();
const spinnerController = useSignal("visible");

const filterStore = useStore({
features: [],
repoOwners: [],
Expand All @@ -59,7 +63,51 @@ export default component$(() => {
},
);

const handleMobileFilter = $(() => (filter.value = !filter.value));
const handleMobileFilter = $((e: Event) => {
filter.value = !filter.value;
const target = e.target as HTMLElement;
const targetSpinner = target.querySelector(".spinner-more");
if (targetSpinner) targetSpinner.classList.toggle("hidden");
});

useOnDocument(
"readystatechange",
$(() => {
// Options for the observer (which mutations to observe)
const config = { childList: true, subtree: true };

// Callback function to execute when mutations are observed
const callback = (mutationList: string | any[]) => {
if (mutationList.length > 0) {
spinnerController.value = "hidden";

window.scrollTo({ top: 0, behavior: "smooth" });
document.querySelector("#spinner-next")?.classList.add("hidden");
document.querySelector("#spinner-prev")?.classList.add("hidden");
const spinners = document.querySelectorAll(".page-number > .spinner");
for (const spinner of spinners) {
spinner.classList.add("hidden");
}

// Reset current page if total items is less than items per page
if (computedProjects.value.total < itemsPerPage) {
currentPage.value = 1;
} else if (
currentPage.value * itemsPerPage >=
computedProjects.value.total
) {
currentPage.value = Math.ceil(
computedProjects.value.total / itemsPerPage,
);
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
if (!projectListRef.value) return;
observer.observe(projectListRef.value, config);
}),
);

return (
<>
Expand All @@ -71,16 +119,18 @@ export default component$(() => {
</div>
</Section>
) : (
<div class="sticky top-0 flex items-center justify-between bg-white p-6 md:hidden">
<div class="sticky top-0 z-10 flex items-center justify-between bg-white p-6 md:hidden">
<h3>{$localize`設定篩選條件`}</h3>
<button
class={[
"flex items-center justify-center gap-4 rounded-md border border-primary bg-white px-3.5 py-2.5 text-base font-semibold text-primary shadow-sm",
"hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600",
"relative",
]}
onClick$={handleMobileFilter}
>
{$localize`完成`}
<span class="spinner-more pointer-events-none absolute hidden"></span>
</button>
</div>
)}
Expand All @@ -99,6 +149,7 @@ export default component$(() => {
q:slot="icon-right"
class="h-5 w-5 text-primary-700"
/>
<span class="spinner-more pointer-events-none absolute hidden"></span>
</button>
)}
<div
Expand All @@ -110,18 +161,21 @@ export default component$(() => {
categoryName={$localize`功能類型`}
filterOptions={filters.features}
store={filterStore}
spinnerController={spinnerController.value}
/>
<Filter
filterName="repoOwners"
categoryName={$localize`提供單位`}
filterOptions={filters.repoOwners}
store={filterStore}
spinnerController={spinnerController.value}
/>
<Filter
filterName="techStacks"
categoryName={$localize`使用技術`}
filterOptions={filters.techStacks}
store={filterStore}
spinnerController={spinnerController.value}
/>
</div>
{filter.value ? (
Expand All @@ -131,25 +185,32 @@ export default component$(() => {
>
<Filter
filterName="features"
categoryName={$localize`包含系統功能`}
categoryName={$localize`功能類型`}
filterOptions={filters.features}
store={filterStore}
spinnerController={"hidden"}
/>
<Filter
filterName="repoOwners"
categoryName={$localize`提供單位`}
filterOptions={filters.repoOwners}
store={filterStore}
spinnerController={"hidden"}
/>
<Filter
filterName="techStacks"
categoryName={$localize`使用技術`}
filterOptions={filters.techStacks}
store={filterStore}
spinnerController={"hidden"}
/>
</div>
) : (
<div id="projects" class="flex w-full flex-col gap-8">
<div
id="projects"
class="flex w-full flex-col gap-8"
ref={projectListRef}
>
{computedProjects.value.data.map((project) => {
const projectName =
project.description["zh-Hant"].localisedName || project.name;
Expand Down
32 changes: 24 additions & 8 deletions src/routes/projects/page-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,22 @@ export const PageNav = component$<PageNavProps>(
({ currentPage, itemsPerPage, totalItems }) => {
const totalPage = Math.ceil(totalItems / itemsPerPage);

const handleNextPage = $(() => {
const handleNextPage = $((e: Event) => {
if (currentPage.value * itemsPerPage >= totalItems) {
return;
}
const target = e.target as HTMLButtonElement;
const spinner = target.querySelector(".spinner");
if (spinner) spinner.classList.remove("hidden");
currentPage.value++;
window.scrollTo({ top: 0, behavior: "smooth" });
});

const handlePrevPage = $(() => {
const handlePrevPage = $((e: Event) => {
if (currentPage.value === 1) return;
currentPage.value--;
window.scrollTo({ top: 0, behavior: "smooth" });
const target = e.target as HTMLButtonElement;
const spinner = target.querySelector(".spinner");
if (spinner) spinner.classList.remove("hidden");
});

const generatePageList = () => {
Expand All @@ -66,15 +70,18 @@ export const PageNav = component$<PageNavProps>(
<button
key={index}
class={[
"page-number",
"group relative w-10 font-medium hover:text-brand-secondary",
currentPage.value === page
? "text-brand-secondary"
: "text-gray-300",
"transition-colors duration-[50ms] ease-out",
]}
onClick$={() => {
onClick$={(e: Event) => {
const target = e.target as HTMLButtonElement;
const spinner = target.querySelector(".spinner");
if (spinner) spinner.classList.remove("hidden");
currentPage.value = page;
window.scrollTo({ top: 0, behavior: "smooth" });
}}
disabled={page === -1}
>
Expand All @@ -88,6 +95,7 @@ export const PageNav = component$<PageNavProps>(
"transition-colors duration-[50ms] ease-out",
]}
></span>
<span class="spinner pointer-events-none absolute right-2 hidden"></span>
</button>
));
};
Expand All @@ -100,9 +108,13 @@ export const PageNav = component$<PageNavProps>(
]}
onClick$={handlePrevPage}
>
<div class="flex gap-3">
<div class="pointer-events-none relative flex gap-3">
<ArrowLeftIcon class="w-5" />
<div class="text-sm font-medium">{$localize`上一頁`}</div>
<div
id="spinner-prev"
class="spinner absolute bottom-0 right-3 hidden"
></div>
</div>
</button>

Expand All @@ -117,9 +129,13 @@ export const PageNav = component$<PageNavProps>(
onClick$={handleNextPage}
disabled={currentPage.value * itemsPerPage >= totalItems}
>
<div class="flex gap-3">
<div class="pointer-events-none relative flex gap-3">
<div class="text-sm font-medium">{$localize`下一頁`}</div>
<ArrowRightIcon class="w-5" />
<div
id="spinner-next"
class="spinner absolute bottom-0 left-3 hidden"
></div>
</div>
</button>
</div>
Expand Down
Loading

0 comments on commit c8daffb

Please sign in to comment.