Skip to content

DK LIBRARY #393

@abhinavkumarsingh938-code

Description

import React, { useEffect, useMemo, useRef, useState } from "react";
import { motion } from "framer-motion";
import { format, addDays } from "date-fns";
import {
CheckCircle2,
ArrowRight,
Users2,
BookOpen,
CalendarClock,
Clock4,
Sofa,
Mail,
Phone,
ShieldCheck,
Search,
Trash2,
RefreshCw,
LockKeyhole,
Sun,
Moon,
Info,
} from "lucide-react";

// shadcn/ui
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";

/**

  • =============================
  • DK LIBRARY — SINGLE FILE APP (UPDATED)
  • React + Tailwind + shadcn/ui + Firebase (Firestore)
  • Focus: One‑time Admission → Fixed Seat+Shift for 30 Days
  • Public Seat Chart + Admin monthly reset
  • =============================
    1. Replace FIREBASE_CONFIG below with your Firebase project's web config
  • (Project Settings → General → Your apps → SDK setup & config).
    1. Deploy this component in a React app (Vite/Next.js) and ensure Tailwind is enabled.
    1. Firestore structure used:
    • admissions: { name, age, phone, email, note,
  •                shiftId, shiftLabel, seatNumber,
    
  •                startDate (yyyy-MM-dd), endDate (yyyy-MM-dd),
    
  •                createdAt }
    
    1. Security: set Firestore rules (template at bottom comment).
      */

// ================= Firebase =================
// Install deps: npm i firebase date-fns framer-motion lucide-react
import { initializeApp } from "firebase/app";
import {
getFirestore,
collection,
addDoc,
query,
where,
orderBy,
onSnapshot,
getDocs,
serverTimestamp,
doc,
deleteDoc,
} from "firebase/firestore";

const FIREBASE_CONFIG = {
apiKey: "REPLACE_ME",
authDomain: "REPLACE_ME.firebaseapp.com",
projectId: "REPLACE_ME",
storageBucket: "REPLACE_ME.appspot.com",
messagingSenderId: "REPLACE_ME",
appId: "REPLACE_ME",
};

const app = initializeApp(FIREBASE_CONFIG);
const db = getFirestore(app);

// ================== App Settings ==================
const LIBRARY_NAME = "DK Library";
const TOTAL_SEATS = 30; // You set 30 seats
// Five fixed 4‑hour shifts (including overnight)
const SHIFTS = [
{ id: "S1", label: "06:00 – 10:00", start: "06:00", end: "10:00" },
{ id: "S2", label: "10:00 – 14:00", start: "10:00", end: "14:00" },
{ id: "S3", label: "14:00 – 18:00", start: "14:00", end: "18:00" },
{ id: "S4", label: "18:00 – 22:00", start: "18:00", end: "22:00" },
{ id: "S5", label: "22:00 – 06:00", start: "22:00", end: "06:00" }, // overnight
];
const ADMIN_PIN = "2468"; // CHANGE ME

function useDarkMode() {
const [dark, setDark] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("dk-dark");
if (saved) setDark(saved === "true");
}, []);
useEffect(() => {
localStorage.setItem("dk-dark", String(dark));
const root = document.documentElement;
if (dark) root.classList.add("dark"); else root.classList.remove("dark");
}, [dark]);
return { dark, setDark };
}

function Container({ children, className = "" }) {
return <div className={mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8 ${className}}>{children};
}

function Header({ onJumpTo }) {
const { dark, setDark } = useDarkMode();
return (




DK

{LIBRARY_NAME}


<Button variant="ghost" onClick={()=>onJumpTo("#admissions")} className="hidden sm:inline-flex gap-2">
Admission

<Button variant="ghost" onClick={()=>onJumpTo("#availability")} className="hidden sm:inline-flex gap-2">
Seat Status

<button
aria-label="Toggle theme"
className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-neutral-200 dark:border-neutral-800"
onClick={() => setDark(!dark)}
>
{dark ? : }




);
}

function Hero() {
return (




<motion.div initial={{ opacity: 0, y: 14 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}
className="mx-auto max-w-3xl text-center">
Welcome to {LIBRARY_NAME}


One‑time Admission & Fixed 4‑Hour Shifts



Choose your seat and shift once and keep it for 30 days. Free booking, instant confirmation.




{["Calm Space", "Wi‑Fi", "30 Seats", "Power Backup"].map((item) => (


{item}

))}

</motion.div>


);
}

// ===== Helpers =====
const todayStr = () => format(new Date(), "yyyy-MM-dd");
const addDaysStr = (d, n) => format(addDays(d, n), "yyyy-MM-dd");

async function phoneHasActiveAdmission(phone) {
const qx = query(
collection(db, "admissions"),
where("phone", "==", phone),
where("endDate", ">=", todayStr())
);
const snap = await getDocs(qx);
// Filter in client for startDate <= today (in case of edge)
const rows = snap.docs.map(d=>({ id: d.id, ...d.data()})).filter(r=>r.startDate <= todayStr());
return rows[0] || null;
}

async function seatTaken(shiftId, seatNumber) {
const qx = query(
collection(db, "admissions"),
where("shiftId", "==", shiftId),
where("seatNumber", "==", seatNumber),
where("endDate", ">=", todayStr())
);
const snap = await getDocs(qx);
const rows = snap.docs.map(d=>({ id: d.id, ...d.data()})).filter(r=>r.startDate <= todayStr());
return rows.length > 0;
}

async function createAdmission(data) {
const ref = await addDoc(collection(db, "admissions"), { ...data, createdAt: serverTimestamp() });
return ref.id;
}

// Listen seats for a shift (active only)
function useActiveSeats(shiftId) {
const [taken, setTaken] = useState([]);
useEffect(() => {
if (!shiftId) return;
const qx = query(
collection(db, "admissions"),
where("shiftId", "==", shiftId),
where("endDate", ">=", todayStr()),
orderBy("seatNumber", "asc")
);
const unsub = onSnapshot(qx, (snap)=>{
const seats = snap.docs
.map(d=>({ id: d.id, ...d.data()}))
.filter(r=>r.startDate <= todayStr())
.map(r=>r.seatNumber);
setTaken(seats);
});
return ()=>unsub();
}, [shiftId]);
return taken;
}

// ===== Public Seat Availability =====
function Availability() {
const [shiftId, setShiftId] = useState(SHIFTS[0].id);
const taken = useActiveSeats(shiftId);
const allSeats = useMemo(()=>Array.from({length: TOTAL_SEATS}, (_,i)=>i+1), []);

return (





Public Seat Status

Check Seats by Shift


Live availability for active 30‑day cycle.







Shift
{SHIFTS.map((s)=> ( {s.label} ))}

Green = available, Grey = booked.

          <div className="grid grid-cols-6 sm:grid-cols-10 gap-2">
            {allSeats.map((n)=>{
              const isTaken = taken.includes(n);
              return (
                <div key={n}
                  className={`h-10 rounded-xl border text-sm grid place-items-center ${isTaken?"bg-neutral-200 dark:bg-neutral-800 text-neutral-400":"bg-green-50 dark:bg-green-900/20"}`}
                >{n}</div>
              );
            })}
          </div>
        </CardContent>
      </Card>
    </div>
  </Container>
</section>

);
}

// ===== Admissions (one time for 30 days) =====
function Admissions() {
const [form, setForm] = useState({ name: "", age: "", phone: "", email: "", note: "" });
const [shiftId, setShiftId] = useState(SHIFTS[0].id);
const [seat, setSeat] = useState("");
const [saving, setSaving] = useState(false);
const [msg, setMsg] = useState("");

const taken = useActiveSeats(shiftId);
const allSeats = useMemo(()=>Array.from({length: TOTAL_SEATS}, (_,i)=>i+1), []);
const freeSeats = allSeats.filter(n=>!taken.includes(n));

const onSubmit = async (e) => {
e.preventDefault();
setMsg("");
if (!form.name || !form.phone || !seat) { setMsg("Name, phone and seat are required."); return; }
setSaving(true);
try {
// already active?
const active = await phoneHasActiveAdmission(form.phone.trim());
if (active) { setMsg("This phone already has an active 30‑day admission."); setSaving(false); return; }
// seat free?
const takenNow = await seatTaken(shiftId, Number(seat));
if (takenNow) { setMsg("Seat just got booked. Please choose another."); setSaving(false); return; }

  const start = todayStr();
  const end = addDaysStr(new Date(), 30); // expires after 30 days
  await createAdmission({
    name: form.name.trim(),
    age: form.age ? Number(form.age) : null,
    phone: form.phone.trim(),
    email: form.email.trim() || null,
    note: form.note.trim() || null,
    shiftId,
    shiftLabel: SHIFTS.find(s=>s.id===shiftId)?.label,
    seatNumber: Number(seat),
    startDate: start,
    endDate: end,
  });
  setMsg(`Admission successful! Seat ${seat} in ${SHIFTS.find(s=>s.id===shiftId)?.label} reserved until ${format(new Date(end), "dd MMM yyyy")}.`);
  setForm({ name: "", age: "", phone: "", email: "", note: "" });
  setSeat("");
} catch (err) {
  console.error(err);
  setMsg("Something went wrong. Please try again.");
} finally {
  setSaving(false);
}

};

return (





Admissions

Fix Your Seat for 30 Days


Choose a shift and seat. One‑time admission, valid for 30 days.





Student Details





Name *
<Input value={form.name} onChange={(e)=>setForm({ ...form, name: e.target.value })} placeholder="Full name"/>


Age
<Input type="number" min={10} value={form.age} onChange={(e)=>setForm({ ...form, age: e.target.value })} placeholder="18"/>




Phone *
<Input value={form.phone} onChange={(e)=>setForm({ ...form, phone: e.target.value })} placeholder="98765 43210"/>


Email
<Input type="email" value={form.email} onChange={(e)=>setForm({ ...form, email: e.target.value })} placeholder="[email protected]"/>



Note (optional)
<Textarea value={form.note} onChange={(e)=>setForm({ ...form, note: e.target.value })} placeholder="Exam/course info"/>

            <Separator/>

            <div className="grid md:grid-cols-3 gap-4">
              <div>
                <Label>Shift *</Label>
                <Select value={shiftId} onValueChange={(v)=>{ setShiftId(v); setSeat(""); }}>
                  <SelectTrigger><SelectValue placeholder="Select shift" /></SelectTrigger>
                  <SelectContent>
                    {SHIFTS.map((s)=> (
                      <SelectItem key={s.id} value={s.id}>{s.label}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
              <div className="md:col-span-2">
                <Label>Choose Seat *</Label>
                <div className="mt-2 grid grid-cols-6 sm:grid-cols-10 gap-2">
                  {Array.from({length: TOTAL_SEATS}, (_,i)=>i+1).map((n)=>{
                    const takenSeat = taken.includes(n);
                    const isSelected = Number(seat)===n;
                    return (
                      <button key={n}
                        type="button"
                        disabled={takenSeat}
                        onClick={()=>setSeat(n)}
                        className={`h-10 rounded-xl border text-sm ${takenSeat?"bg-neutral-200 dark:bg-neutral-800 text-neutral-400 cursor-not-allowed":"hover:border-indigo-400 bg-green-50 dark:bg-green-900/20"} ${isSelected?"ring-2 ring-indigo-500 border-indigo-500":""}`}
                        title={takenSeat?"Booked":"Available"}
                      >
                        {n}
                      </button>
                    );
                  })}
                </div>
                <div className="mt-2 text-xs text-neutral-500">(Grey = booked, Green = available)</div>
              </div>
            </div>

            <div className="flex items-center justify-between">
              <div className="text-sm text-neutral-500">Data is used only for seat management at {LIBRARY_NAME}.</div>
              <Button type="submit" disabled={saving || !seat} className="inline-flex gap-2">{saving?"Submitting...":"Confirm Admission"} <ArrowRight className="h-4 w-4"/></Button>
            </div>
            {msg && <div className="text-sm mt-2 text-indigo-600 dark:text-indigo-300">{msg}</div>}
          </form>
        </CardContent>
      </Card>
    </div>
  </Container>
</section>

);
}

// ===== Admin Panel =====
function AdminPanel() {
const [pin, setPin] = useState("");
const [ok, setOk] = useState(false);
const [shiftId, setShiftId] = useState(SHIFTS[0].id);
const [searchPhone, setSearchPhone] = useState("");
const [rows, setRows] = useState([]);

// Active admissions stream
useEffect(()=>{
if (!ok) return;
const qx = query(
collection(db, "admissions"),
where("endDate", ">=", todayStr()),
orderBy("seatNumber", "asc")
);
const unsub = onSnapshot(qx, (snap)=>{
const r = snap.docs.map(d=>({ id: d.id, ...d.data()})).filter(x=>x.startDate <= todayStr());
setRows(r);
});
return ()=>unsub();
}, [ok]);

const takenByShift = useMemo(()=>{
return rows.filter(r=>r.shiftId===shiftId).map(r=>r.seatNumber);
}, [rows, shiftId]);

const handleDelete = async (id) => {
if (!ok) return;
if (!confirm("Cancel this admission?")) return;
await deleteDoc(doc(db, "admissions", id));
};

const handleMonthlyReset = async () => {
if (!ok) return;
if (!confirm("Reset ALL active admissions for a new 30‑day cycle? This cannot be undone.")) return;
// delete all docs currently loaded (active)
for (const r of rows) {
await deleteDoc(doc(db, "admissions", r.id));
}
};

const filteredList = useMemo(()=>{
if (!searchPhone.trim()) return rows;
return rows.filter(r=> r.phone?.includes(searchPhone.trim()));
}, [rows, searchPhone]);

return (






Admin

Dashboard


Manage 30‑day admissions, see seat map, and reset monthly.



{!ok && (


Enter PIN
<Input type="password" value={pin} onChange={(e)=>setPin(e.target.value)} placeholder="Admin PIN" className="w-40"/>

<Button onClick={()=> setOk(pin===ADMIN_PIN)} className="inline-flex gap-2"> Unlock

)}

      {!ok ? (
        <Card className="rounded-2xl">
          <CardContent className="py-10 grid place-items-center text-neutral-500">
            <div className="flex items-center gap-2"><ShieldCheck className="h-5 w-5"/> Admin access required.</div>
          </CardContent>
        </Card>
      ) : (
        <Tabs defaultValue="map" className="w-full">
          <TabsList>
            <TabsTrigger value="map">Seat Chart</TabsTrigger>
            <TabsTrigger value="list">Student List</TabsTrigger>
            <TabsTrigger value="settings">Reset</TabsTrigger>
          </TabsList>

          <TabsContent value="map">
            <Card className="rounded-2xl mt-4">
              <CardHeader>
                <CardTitle className="text-lg flex items-center gap-2"><CalendarClock className="h-5 w-4"/> Seat Status (Admin)</CardTitle>
              </CardHeader>
              <CardContent className="grid gap-6">
                <div className="grid md:grid-cols-4 gap-4">
                  <div>
                    <Label>Shift</Label>
                    <Select value={shiftId} onValueChange={setShiftId}>
                      <SelectTrigger><SelectValue placeholder="Select shift" /></SelectTrigger>
                      <SelectContent>
                        {SHIFTS.map((s)=> (
                          <SelectItem key={s.id} value={s.id}>{s.label}</SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </div>
                  <div className="md:col-span-3 text-sm text-neutral-500 flex items-end">Click a booked seat to see student details.</div>
                </div>

                <div className="grid grid-cols-6 sm:grid-cols-10 gap-2">
                  {Array.from({length: TOTAL_SEATS}, (_,i)=>i+1).map((n)=>{
                    const booked = takenByShift.includes(n);
                    const student = rows.find(r=>r.shiftId===shiftId && r.seatNumber===n);
                    return (
                      <button key={n}
                        disabled={!booked}
                        onClick={()=> booked && alert(`${student?.name||"Unknown"} (☎️ ${student?.phone||""})

Seat ${n} • ${student?.shiftLabel}
Valid: ${student?.startDate} → ${student?.endDate})} className={h-10 rounded-xl border text-sm ${booked?"bg-neutral-200 dark:bg-neutral-800":"bg-green-50 dark:bg-green-900/20"}} title={booked?Booked by ${student?.name}`:"Available"}
>
{n}

);
})}




          <TabsContent value="list">
            <Card className="rounded-2xl mt-4">
              <CardHeader>
                <CardTitle className="text-lg">Active Admissions</CardTitle>
              </CardHeader>
              <CardContent className="grid gap-4">
                <div className="grid md:grid-cols-2 gap-4">
                  <div className="md:col-span-1">
                    <Label>Search by phone</Label>
                    <div className="flex gap-2">
                      <Input value={searchPhone} onChange={(e)=>setSearchPhone(e.target.value)} placeholder="e.g. 98765"/>
                      <Button variant="outline" className="inline-flex gap-2"><Search className="h-4 w-4"/> Search</Button>
                    </div>
                  </div>
                </div>

                <div className="overflow-x-auto">
                  <table className="min-w-full text-sm">
                    <thead>
                      <tr className="text-left border-b dark:border-neutral-800">
                        <th className="py-2 pr-4">Seat</th>
                        <th className="py-2 pr-4">Shift</th>
                        <th className="py-2 pr-4">Student</th>
                        <th className="py-2 pr-4">Phone</th>
                        <th className="py-2 pr-4">Email</th>
                        <th className="py-2 pr-4">Valid Till</th>
                        <th className="py-2 pr-4">Actions</th>
                      </tr>
                    </thead>
                    <tbody>
                      {filteredList.map((b)=> (
                        <tr key={b.id} className="border-b last:border-0 dark:border-neutral-800">
                          <td className="py-2 pr-4">{b.seatNumber}</td>
                          <td className="py-2 pr-4">{b.shiftLabel}</td>
                          <td className="py-2 pr-4">{b.name}</td>
                          <td className="py-2 pr-4">{b.phone}</td>
                          <td className="py-2 pr-4">{b.email||"—"}</td>
                          <td className="py-2 pr-4">{format(new Date(b.endDate), "dd MMM yyyy")}</td>
                          <td className="py-2 pr-4">
                            <Button variant="outline" size="sm" onClick={()=>handleDelete(b.id)} className="inline-flex gap-2"><Trash2 className="h-4 w-4"/> Cancel</Button>
                          </td>
                        </tr>
                      ))}
                      {filteredList.length===0 && (
                        <tr><td className="py-6 text-neutral-500" colSpan={7}>No active admissions.</td></tr>
                      )}
                    </tbody>
                  </table>
                </div>
              </CardContent>
            </Card>
          </TabsContent>

          <TabsContent value="settings">
            <Card className="rounded-2xl mt-4">
              <CardHeader>
                <CardTitle className="text-lg flex items-center gap-2"><RefreshCw className="h-5 w-4"/> Monthly Reset</CardTitle>
              </CardHeader>
              <CardContent className="grid gap-4">
                <p className="text-sm text-neutral-600 dark:text-neutral-300">Clicking reset will delete all active admissions to start a new 30‑day cycle.</p>
                <div>
                  <Button variant="destructive" onClick={handleMonthlyReset} className="inline-flex gap-2"><RefreshCw className="h-4 w-4"/> Reset All Seats</Button>
                </div>
              </CardContent>
            </Card>
          </TabsContent>
        </Tabs>
      )}
    </div>
  </Container>
</section>

);
}

function Footer() {
return (



© {new Date().getFullYear()} {LIBRARY_NAME}. All rights reserved.



+91‑90000‑00000




);
}

export default function DKLibraryApp() {
const rootRef = useRef(null);
const onJumpTo = (id) => {
const el = document.querySelector(id);
el?.scrollIntoView({ behavior: "smooth" });
};

return (







  {/* ===== Firestore Rules (paste in Firebase console → Firestore → Rules) =====
  rules_version = '2';
  service cloud.firestore {
    match /databases/{database}/documents {
      function isPhoneValid(p) { return p.size() >= 8 && p.size() <= 15; }
      function validDate(s) { return s.matches('\d{4}-\d{2}-\d{2}'); }

      match /admissions/{id} {
        allow read: if true; // public seat status
        allow create: if request.resource.data.name is string
                       && isPhoneValid(request.resource.data.phone)
                       && request.resource.data.shiftId in ['S1','S2','S3','S4','S5']
                       && request.resource.data.seatNumber is int
                       && request.resource.data.seatNumber >= 1 && request.resource.data.seatNumber <= 30
                       && validDate(request.resource.data.startDate)
                       && validDate(request.resource.data.endDate)
                       && request.resource.data.createdAt == request.time;
        // TEMP: allow delete for admin panel until Auth is added
        allow delete: if true;
        allow update: if false;
      }
    }
  }
  */}
</div>

);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions