Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .env.example

This file was deleted.

45 changes: 24 additions & 21 deletions src/app/saved/saved-mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Footer from '@/components/ui/Footer';
import Image from 'next/image';
import axios from 'axios';
import { PopupViewTT } from '@/components/ui/PopupMobile';
import Loader from '@/components/ui/Loader';
import SavedMobileSkeleton from '@/components/ui/SavedMobileSkeleton';

async function fetchTimetablesByOwner(owner: string) {
const res = await axios.get(`/api/timetables?owner=${encodeURIComponent(owner)}`);
Expand Down Expand Up @@ -115,32 +115,35 @@ export default function SavedMobile() {

<div className="text-4xl mb-8 mt-28 text-black font-pangolin">Saved Timetables</div>

<ul className="w-full space-y-4 px-6">
{timetables.map((tt, index) => (
<li
key={tt._id}
className="grid grid-cols-[auto_1fr] items-center px-3 py-3 bg-[#A7D5D7] text-black rounded-xl border-2 border-black shadow-[2px_2px_0_0_black]"
onClick={() => handleView(tt)}
>
<span className="font-medium text-base mr-4">{index + 1}.</span>
<span className="font-medium text-base truncate">{tt.title}</span>
</li>
))}
</ul>
{loading ? (
<SavedMobileSkeleton rows={6} />
) : (
<ul className="w-full space-y-4 px-6">
{timetables.map((tt, index) => (
<li
key={tt._id}
className="grid grid-cols-[auto_1fr] items-center px-3 py-3 bg-[#A7D5D7] text-black rounded-xl border-2 border-black shadow-[2px_2px_0_0_black]"
onClick={() => handleView(tt)}
>
<span className="font-medium text-base mr-4">{index + 1}.</span>
<span className="font-medium text-base truncate">{tt.title}</span>
</li>
))}
</ul>
)}

<div className="flex items-center w-full mt-8 mb-8 text-sm font-poppins font-semibold text-black/50 px-8">
<div className="flex-grow h-0.5 bg-gradient-to-r from-transparent to-black/33" />
{loading ? null : timetables.length === 0 ? (
<span className="mx-4">Nothing To Show Here</span>
) : (
<span className="mx-4">End of List</span>
)}
{!loading &&
(timetables.length === 0 ? (
<span className="mx-4">Nothing To Show Here</span>
) : (
<span className="mx-4">End of List</span>
))}
<div className="flex-grow h-0.5 bg-gradient-to-r from-black/33 to-transparent" />
</div>

{loading ? (
<Loader />
) : timetables.length === 0 ? (
{!loading && timetables.length === 0 ? (
<div className="mx-auto my-auto text-center text-sm font-poppins font-semibold text-black/70">
No saved timetables found.
<br />
Expand Down
114 changes: 60 additions & 54 deletions src/app/saved/saved.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Image from 'next/image';
import AlertModal from '@/components/ui/AlertModal';
import axios from 'axios';
import Loader from '@/components/ui/Loader';
import SavedSkeleton from '@/components/ui/SavedSkeleton';

async function fetchTimetablesByOwner(owner: string) {
const res = await axios.get(`/api/timetables?owner=${encodeURIComponent(owner)}`);
Expand Down Expand Up @@ -157,63 +158,68 @@ export default function Saved() {
<div className="flex-1 flex flex-col items-center">
<h1 className="text-6xl mt-48 mb-16 font-pangolin">Saved Timetables</h1>
<div className="w-5/6 max-w-7xl rounded-[60px] border-4 border-black bg-[#A7D5D7] p-12 mb-24 shadow-[4px_4px_0_0_black]">
<h2 className="text-4xl mb-8 font-pangolin font-light">All Timetables</h2>

{loading ? (
<Loader />
/* ── skeleton replaces both the heading and the list while fetching ── */
<SavedSkeleton rows={5} />
) : timetables.length === 0 ? (
<div className="flex flex-col items-center">
<p className="text-3xl mb-6">(No Timetables Found)</p>
<ZButton
onClick={() => router.push('/')}
type="large"
text="Home"
color="purple"
image="/icons/home.svg"
/>
</div>
<>
<h2 className="text-4xl mb-8 font-pangolin font-light">All Timetables</h2>
<div className="flex flex-col items-center">
<p className="text-3xl mb-6">(No Timetables Found)</p>
<ZButton
onClick={() => router.push('/')}
type="large"
text="Home"
color="purple"
image="/icons/home.svg"
/>
</div>
</>
) : (
<ul className="space-y-4 max-h-[60vh] overflow-y-auto pr-4">
{timetables.map((tt, i) => (
<li
key={tt._id}
className="flex items-center justify-between bg-[#C9E5E6] p-5 rounded-4xl"
>
<span className="text-xl">
{i + 1}. {tt.title}
</span>
<div className="flex gap-2">
<ZButton
type="image"
color="yellow"
image="/icons/eye.svg"
onClick={() => openView(tt)}
/>
<ZButton
type="image"
color="blue"
image="/icons/edit.svg"
onClick={() => {
setSelectedTT(tt);
setRenameValue(tt.title);
setPopupType('rename_tt');
setShowPopup(true);
}}
/>
<ZButton
type="image"
color="red"
image="/icons/trash.svg"
onClick={() => {
setSelectedTT(tt);
setPopupType('delete_tt');
setShowPopup(true);
}}
/>
</div>
</li>
))}
</ul>
<>
<h2 className="text-4xl mb-8 font-pangolin font-light">All Timetables</h2>
<ul className="space-y-4 max-h-[60vh] overflow-y-auto pr-4">
{timetables.map((tt, i) => (
<li
key={tt._id}
className="flex items-center justify-between bg-[#C9E5E6] p-5 rounded-4xl"
>
<span className="text-xl">
{i + 1}. {tt.title}
</span>
<div className="flex gap-2">
<ZButton
type="image"
color="yellow"
image="/icons/eye.svg"
onClick={() => openView(tt)}
/>
<ZButton
type="image"
color="blue"
image="/icons/edit.svg"
onClick={() => {
setSelectedTT(tt);
setRenameValue(tt.title);
setPopupType('rename_tt');
setShowPopup(true);
}}
/>
<ZButton
type="image"
color="red"
image="/icons/trash.svg"
onClick={() => {
setSelectedTT(tt);
setPopupType('delete_tt');
setShowPopup(true);
}}
/>
</div>
</li>
))}
</ul>
</>
)}
</div>
</div>
Expand Down
54 changes: 54 additions & 0 deletions src/components/ui/SavedMobileSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* SavedMobileSkeleton.tsx
* Drop into: src/components/ui/SavedMobileSkeleton.tsx
*
* Usage in saved-mobile.tsx:
* Replace the entire <ul> + <Loader /> loading branches with:
* {loading ? <SavedMobileSkeleton rows={6} /> : <ul>...</ul>}
*/

import React from 'react';

const SHIMMER_CSS = `
@keyframes boneShimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.bone-mobile {
background: linear-gradient(
90deg,
#b8d9db 25%,
#d6ecee 50%,
#b8d9db 75%
);
background-size: 200% 100%;
animation: boneShimmer 1.6s ease-in-out infinite;
}
`;

function BoneRow({ delay }: { delay: string }) {
return (
<li
className="bone-mobile rounded-xl border-2 border-black shadow-[2px_2px_0_0_black]"
style={{ height: 52, animationDelay: delay, listStyle: 'none' }}
aria-hidden="true"
/>
);
}

interface SavedMobileSkeletonProps {
rows?: number;
}

export default function SavedMobileSkeleton({ rows = 6 }: SavedMobileSkeletonProps) {
return (
<>
<style dangerouslySetInnerHTML={{ __html: SHIMMER_CSS }} />
<ul className="w-full space-y-4 px-6" style={{ padding: '0 1.5rem' }}>
{Array.from({ length: rows }).map((_, i) => (
<BoneRow key={i} delay={`${i * 90}ms`} />
))}
</ul>
</>
);
}
103 changes: 103 additions & 0 deletions src/components/ui/SavedSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* SavedSkeleton.tsx
* Drop this file into: src/components/ui/SavedSkeleton.tsx
*
* Usage in saved.tsx — replace:
* {loading ? <Loader /> : ...}
* with:
* {loading ? <SavedSkeleton rows={5} /> : ...}
*/

import React from 'react';

/* ─────────────────────────────────────────────
Inline keyframes injected once per mount.
(Tailwind's animate-pulse is fine for a solid
flash, but we want the classic "bone shimmer"
highlight sweep that travels left → right.)
───────────────────────────────────────────── */
const SHIMMER_CSS = `
@keyframes boneShimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.bone {
background: linear-gradient(
90deg,
#b8d9db 25%,
#d6ecee 50%,
#b8d9db 75%
);
background-size: 200% 100%;
animation: boneShimmer 1.6s ease-in-out infinite;
border-radius: 9999px;
}
`;

/* ── one skeleton row that mirrors a real <li> ── */
function BoneRow({ index, delay }: { index: number; delay: string }) {
return (
<li
className="flex items-center justify-between bg-[#C9E5E6] p-5 rounded-4xl"
style={{ animationDelay: delay }}
>
{/* left: index dot + title bar */}
<div className="flex items-center gap-3">
{/* tiny index circle */}
<span
className="bone"
style={{ width: 24, height: 24, display: 'inline-block', animationDelay: delay }}
/>
{/* title bar — varies width slightly so rows look natural */}
<span
className="bone"
style={{
width: `${160 + (index % 3) * 48}px`,
height: 22,
display: 'inline-block',
animationDelay: delay,
}}
/>
</div>
</li>
);
}

/* ── header bone (matches the "All Timetables" h2) ── */
function BoneHeader() {
return (
<span
className="bone"
style={{
display: 'block',
width: 220,
height: 36,
marginBottom: '2rem',
}}
/>
);
}

/* ── public API ── */
interface SavedSkeletonProps {
/** number of placeholder rows to show (default 5) */
rows?: number;
}

export default function SavedSkeleton({ rows = 5 }: SavedSkeletonProps) {
return (
<>
{/* inject shimmer keyframes exactly once */}
<style dangerouslySetInnerHTML={{ __html: SHIMMER_CSS }} />

{/* ghost h2 */}
<BoneHeader />

<ul className="space-y-4">
{Array.from({ length: rows }).map((_, i) => (
<BoneRow key={i} index={i} delay={`${i * 90}ms`} />
))}
</ul>
</>
);
}