added category infinite grid and fixed missing items at useInfiniteQuery

This commit is contained in:
mr-shortman 2025-03-15 15:20:17 +01:00
parent f591e18847
commit 4ad0357133
15 changed files with 321 additions and 1100 deletions

View File

@ -1,302 +0,0 @@
[
{
"title": "Account Executive"
},
{
"title": "Engineer II"
},
{
"title": "Data Coordinator"
},
{
"title": "Senior Editor"
},
{
"title": "Senior Quality Engineer"
},
{
"title": "Statistician III"
},
{
"title": "Programmer I"
},
{
"title": "Office Assistant II"
},
{
"title": "VP Marketing"
},
{
"title": "Senior Quality Engineer"
},
{
"title": "Business Systems Development Analyst"
},
{
"title": "Chemical Engineer"
},
{
"title": "Director of Sales"
},
{
"title": "Chief Design Engineer"
},
{
"title": "Editor"
},
{
"title": "Speech Pathologist"
},
{
"title": "Pharmacist"
},
{
"title": "Operator"
},
{
"title": "Human Resources Assistant III"
},
{
"title": "Computer Systems Analyst II"
},
{
"title": "Sales Associate"
},
{
"title": "Desktop Support Technician"
},
{
"title": "Executive Secretary"
},
{
"title": "Quality Control Specialist"
},
{
"title": "Research Associate"
},
{
"title": "Software Consultant"
},
{
"title": "Staff Scientist"
},
{
"title": "Senior Sales Associate"
},
{
"title": "Business Systems Development Analyst"
},
{
"title": "VP Sales"
},
{
"title": "Mechanical Systems Engineer"
},
{
"title": "Information Systems Manager"
},
{
"title": "Internal Auditor"
},
{
"title": "Product Engineer"
},
{
"title": "Legal Assistant"
},
{
"title": "GIS Technical Architect"
},
{
"title": "Software Consultant"
},
{
"title": "Paralegal"
},
{
"title": "Nurse"
},
{
"title": "Biostatistician II"
},
{
"title": "Web Designer I"
},
{
"title": "Financial Analyst"
},
{
"title": "Administrative Officer"
},
{
"title": "VP Accounting"
},
{
"title": "Biostatistician IV"
},
{
"title": "Data Coordinator"
},
{
"title": "Occupational Therapist"
},
{
"title": "Web Developer IV"
},
{
"title": "Quality Control Specialist"
},
{
"title": "General Manager"
},
{
"title": "Assistant Manager"
},
{
"title": "Sales Associate"
},
{
"title": "VP Marketing"
},
{
"title": "Graphic Designer"
},
{
"title": "Operator"
},
{
"title": "Senior Financial Analyst"
},
{
"title": "Information Systems Manager"
},
{
"title": "Tax Accountant"
},
{
"title": "Research Assistant II"
},
{
"title": "Quality Engineer"
},
{
"title": "Staff Scientist"
},
{
"title": "Account Representative I"
},
{
"title": "Clinical Specialist"
},
{
"title": "Web Developer II"
},
{
"title": "Desktop Support Technician"
},
{
"title": "Electrical Engineer"
},
{
"title": "Registered Nurse"
},
{
"title": "Paralegal"
},
{
"title": "Financial Advisor"
},
{
"title": "Senior Cost Accountant"
},
{
"title": "Senior Financial Analyst"
},
{
"title": "Safety Technician III"
},
{
"title": "Recruiting Manager"
},
{
"title": "Engineer III"
},
{
"title": "Social Worker"
},
{
"title": "Assistant Manager"
},
{
"title": "Financial Analyst"
},
{
"title": "Health Coach II"
},
{
"title": "Database Administrator III"
},
{
"title": "Senior Editor"
},
{
"title": "Research Nurse"
},
{
"title": "Graphic Designer"
},
{
"title": "Quality Engineer"
},
{
"title": "Media Manager II"
},
{
"title": "Payment Adjustment Coordinator"
},
{
"title": "Desktop Support Technician"
},
{
"title": "Legal Assistant"
},
{
"title": "Research Associate"
},
{
"title": "Operator"
},
{
"title": "Speech Pathologist"
},
{
"title": "Senior Editor"
},
{
"title": "Financial Analyst"
},
{
"title": "Professor"
},
{
"title": "Registered Nurse"
},
{
"title": "Electrical Engineer"
},
{
"title": "Actuary"
},
{
"title": "Nuclear Power Engineer"
},
{
"title": "Social Worker"
},
{
"title": "Safety Technician IV"
},
{
"title": "Web Developer I"
}
]

View File

@ -1,302 +0,0 @@
[
{
"name": "Green Sotol"
},
{
"name": "Mexican Prairie Clover"
},
{
"name": "Stipulate Leaf-flower"
},
{
"name": "Kawelu"
},
{
"name": "Spiked Crested Coralroot"
},
{
"name": "Texas Windmill Grass"
},
{
"name": "Mountain Alder"
},
{
"name": "Macdougal Verbena"
},
{
"name": "European Aspen"
},
{
"name": "Torrey's Willowherb"
},
{
"name": "Ailanthus"
},
{
"name": "Shortstalk Stinkweed"
},
{
"name": "Black Damar"
},
{
"name": "Canelillo"
},
{
"name": "Veatch's Island Broom"
},
{
"name": "Lewton's Milkwort"
},
{
"name": "Hungarian Milkvetch"
},
{
"name": "Palmer Evening Primrose"
},
{
"name": "Smooth Chastetree"
},
{
"name": "Jaeger's Joshua Tree"
},
{
"name": "Roughhairy Maiden Fern"
},
{
"name": "Florida Orchid"
},
{
"name": "Belonia Lichen"
},
{
"name": "Bigfruit Evening Primrose"
},
{
"name": "Fitch's Tarweed"
},
{
"name": "Coastal Plain Dawnflower"
},
{
"name": "Clokey's Gilia"
},
{
"name": "Indian Jointvetch"
},
{
"name": "Wreath Lichen"
},
{
"name": "Cumberland Xanthoparmelia Lichen"
},
{
"name": "Spectacular Flatsedge"
},
{
"name": "Pride Of California"
},
{
"name": "Feverfew"
},
{
"name": "Comb Wash Buckwheat"
},
{
"name": "Sweet Woodreed"
},
{
"name": "Delicate Violet Orchid"
},
{
"name": "Canadian Blacksnakeroot"
},
{
"name": "Wax Currant"
},
{
"name": "Western Mountain Ash"
},
{
"name": "Rhodomyrtus"
},
{
"name": "Johnston's Knotweed"
},
{
"name": "Kauai Bur Cucumber"
},
{
"name": "Cain's Reedgrass"
},
{
"name": "San Diego Pitchersage"
},
{
"name": "Rock Goldenrod"
},
{
"name": "Itchgrass"
},
{
"name": "Threadleaf Horsebrush"
},
{
"name": "Red Hills Vervain"
},
{
"name": "Louisiana Bluestar"
},
{
"name": "Utah Sweetvetch"
},
{
"name": "Kauila"
},
{
"name": "Sea Hibiscus"
},
{
"name": "Derris"
},
{
"name": "Florida Tasselflower"
},
{
"name": "Glossy Hawthorn"
},
{
"name": "Ahlner's Microcalicium Lichen"
},
{
"name": "Aster"
},
{
"name": "Elegant Hawthorn"
},
{
"name": "Pricklypear"
},
{
"name": "Parry's Sage"
},
{
"name": "Redberry Buckthorn"
},
{
"name": "Baden's Bluegrass"
},
{
"name": "Utah Columbine"
},
{
"name": "Obscure Shield Lichen"
},
{
"name": "Showy Orchid"
},
{
"name": "Silverleafed Princess Flower"
},
{
"name": "Oahu Stenogyne"
},
{
"name": "Hammond's Claytonia"
},
{
"name": "Owyhee River Stickseed"
},
{
"name": "Southwestern Cosmos"
},
{
"name": "Toothed Flatsedge"
},
{
"name": "Vegetable Fern"
},
{
"name": "Rose"
},
{
"name": "Desert Wishbone-bush"
},
{
"name": "Rocky Mountain Woodsia"
},
{
"name": "East Indian Lemongrass"
},
{
"name": "Coville's Erigeron"
},
{
"name": "Spiral Flag"
},
{
"name": "Nevada Milkvetch"
},
{
"name": "Douglas's Catchfly"
},
{
"name": "Silverleaf Phacelia"
},
{
"name": "Canadian Ricegrass"
},
{
"name": "Barrier Range Wattle"
},
{
"name": "Brooks' Alsophila"
},
{
"name": "Calder's Bladderpod"
},
{
"name": "Desert Brickellbush"
},
{
"name": "Echeveria"
},
{
"name": "Caruzo"
},
{
"name": "American Black Nightshade"
},
{
"name": "Whiteflower Goldenbush"
},
{
"name": "Littleleaf Milkwort"
},
{
"name": "Fir Mistletoe"
},
{
"name": "Disc Lichen"
},
{
"name": "Flagstaff Rockcress"
},
{
"name": "Golden Spiderflower"
},
{
"name": "Yellow Fumewort"
},
{
"name": "Dot Lichen"
},
{
"name": "Ross' Avens"
},
{
"name": "Sierra Bluecup"
},
{
"name": "Sausage Tree"
}
]

View File

@ -1,417 +0,0 @@
[
{
"name": "Léandre",
"email": "amant0@hc360.com",
"image": "https://robohash.org/autnihilquasi.png?size=50x50&set=set1"
},
{
"name": "Céline",
"email": "aedsall1@zdnet.com",
"image": "https://robohash.org/temporibusfacerevoluptatem.png?size=50x50&set=set1"
},
{
"name": "Vénus",
"email": "bcovington2@dion.ne.jp",
"image": "https://robohash.org/architectoconsecteturconsequatur.png?size=50x50&set=set1"
},
{
"name": "Loïca",
"email": "msteinhammer3@columbia.edu",
"image": "https://robohash.org/pariaturullamaut.png?size=50x50&set=set1"
},
{
"name": "Táng",
"email": "fmichurin4@is.gd",
"image": "https://robohash.org/eaquevoluptasfugit.png?size=50x50&set=set1"
},
{
"name": "Andréa",
"email": "kbygrave5@pen.io",
"image": "https://robohash.org/eligendinatusa.png?size=50x50&set=set1"
},
{
"name": "Léandre",
"email": "bbargery6@ocn.ne.jp",
"image": "https://robohash.org/estidquod.png?size=50x50&set=set1"
},
{
"name": "Chloé",
"email": "mharnor7@ameblo.jp",
"image": "https://robohash.org/facilisducimuset.png?size=50x50&set=set1"
},
{
"name": "Cunégonde",
"email": "lgiovanardi8@liveinternet.ru",
"image": "https://robohash.org/perspiciatisaliquidsit.png?size=50x50&set=set1"
},
{
"name": "Dafnée",
"email": "ebleiman9@reuters.com",
"image": "https://robohash.org/quiquiaquaerat.png?size=50x50&set=set1"
},
{
"name": "Laurène",
"email": "ibostona@tuttocitta.it",
"image": "https://robohash.org/quossolutaenim.png?size=50x50&set=set1"
},
{
"name": "Pénélope",
"email": "kpedrollob@nyu.edu",
"image": "https://robohash.org/molestiasnullaomnis.png?size=50x50&set=set1"
},
{
"name": "Laïla",
"email": "noransc@symantec.com",
"image": "https://robohash.org/rerumharumut.png?size=50x50&set=set1"
},
{
"name": "Yáo",
"email": "hsived@oaic.gov.au",
"image": "https://robohash.org/etiuremolestias.png?size=50x50&set=set1"
},
{
"name": "Judicaël",
"email": "nfockee@wordpress.org",
"image": "https://robohash.org/quisquamestaccusamus.png?size=50x50&set=set1"
},
{
"name": "Léane",
"email": "nalwenf@hexun.com",
"image": "https://robohash.org/quasimagnamab.png?size=50x50&set=set1"
},
{
"name": "Publicité",
"email": "bgeorgeg@howstuffworks.com",
"image": "https://robohash.org/repellendusquiculpa.png?size=50x50&set=set1"
},
{
"name": "Kuí",
"email": "celdonh@blinklist.com",
"image": "https://robohash.org/doloremundequo.png?size=50x50&set=set1"
},
{
"name": "Clémence",
"email": "mcommussoi@scribd.com",
"image": "https://robohash.org/quidoloresat.png?size=50x50&set=set1"
},
{
"name": "Agnès",
"email": "nzambonj@nymag.com",
"image": "https://robohash.org/etnemoconsequatur.png?size=50x50&set=set1"
},
{
"name": "Nadège",
"email": "mwhithamk@bbb.org",
"image": "https://robohash.org/accusamushicest.png?size=50x50&set=set1"
},
{
"name": "Miléna",
"email": "glisettl@merriam-webster.com",
"image": "https://robohash.org/veniamplaceatadipisci.png?size=50x50&set=set1"
},
{
"name": "Séréna",
"email": "wpoundsm@yellowpages.com",
"image": "https://robohash.org/nihiladea.png?size=50x50&set=set1"
},
{
"name": "Chloé",
"email": "sescalen@imgur.com",
"image": "https://robohash.org/optiovoluptatibusaut.png?size=50x50&set=set1"
},
{
"name": "Maëlyss",
"email": "nyoskowitzo@omniture.com",
"image": "https://robohash.org/expeditanobismaxime.png?size=50x50&set=set1"
},
{
"name": "Cunégonde",
"email": "awellenp@de.vu",
"image": "https://robohash.org/commodiutest.png?size=50x50&set=set1"
},
{
"name": "Valérie",
"email": "zduchatelq@imageshack.us",
"image": "https://robohash.org/omnisdoloremtemporibus.png?size=50x50&set=set1"
},
{
"name": "Aloïs",
"email": "foxburyr@github.io",
"image": "https://robohash.org/cumnemoquia.png?size=50x50&set=set1"
},
{
"name": "Eliès",
"email": "bmactimpanys@moonfruit.com",
"image": "https://robohash.org/eosvitaeveritatis.png?size=50x50&set=set1"
},
{
"name": "Maëline",
"email": "abaget@gnu.org",
"image": "https://robohash.org/magnivoluptatibussoluta.png?size=50x50&set=set1"
},
{
"name": "Pål",
"email": "ssunocku@discuz.net",
"image": "https://robohash.org/accusamusquaedelectus.png?size=50x50&set=set1"
},
{
"name": "Irène",
"email": "reneasv@sphinn.com",
"image": "https://robohash.org/assumendahicperferendis.png?size=50x50&set=set1"
},
{
"name": "Renée",
"email": "dmclarenw@skyrock.com",
"image": "https://robohash.org/voluptatemrecusandaeest.png?size=50x50&set=set1"
},
{
"name": "Léonore",
"email": "breimsx@tmall.com",
"image": "https://robohash.org/natusporropariatur.png?size=50x50&set=set1"
},
{
"name": "Táng",
"email": "tcolty@usnews.com",
"image": "https://robohash.org/autenimlabore.png?size=50x50&set=set1"
},
{
"name": "Ruì",
"email": "bblazaz@mac.com",
"image": "https://robohash.org/quosiuremaiores.png?size=50x50&set=set1"
},
{
"name": "Anaëlle",
"email": "vsimon10@npr.org",
"image": "https://robohash.org/velmolestiasullam.png?size=50x50&set=set1"
},
{
"name": "Clémence",
"email": "bedmonds11@economist.com",
"image": "https://robohash.org/consequaturimpeditmollitia.png?size=50x50&set=set1"
},
{
"name": "Eloïse",
"email": "jmaster12@guardian.co.uk",
"image": "https://robohash.org/autdolormolestiae.png?size=50x50&set=set1"
},
{
"name": "Adélaïde",
"email": "mchazier13@slate.com",
"image": "https://robohash.org/laborereprehenderitsequi.png?size=50x50&set=set1"
},
{
"name": "Séverine",
"email": "dgisborne14@rediff.com",
"image": "https://robohash.org/quiavoluptatemet.png?size=50x50&set=set1"
},
{
"name": "Publicité",
"email": "gjobson15@vk.com",
"image": "https://robohash.org/eteaet.png?size=50x50&set=set1"
},
{
"name": "Miléna",
"email": "bskarin16@buzzfeed.com",
"image": "https://robohash.org/facerenecessitatibussuscipit.png?size=50x50&set=set1"
},
{
"name": "Méryl",
"email": "jcurrington17@dropbox.com",
"image": "https://robohash.org/molestiasasperioresmollitia.png?size=50x50&set=set1"
},
{
"name": "Dù",
"email": "aandroli18@infoseek.co.jp",
"image": "https://robohash.org/namillumquo.png?size=50x50&set=set1"
},
{
"name": "Liè",
"email": "hcornwall19@mozilla.com",
"image": "https://robohash.org/dignissimosconsequaturanimi.png?size=50x50&set=set1"
},
{
"name": "Estève",
"email": "mshoutt1a@amazonaws.com",
"image": "https://robohash.org/velitsolutatotam.png?size=50x50&set=set1"
},
{
"name": "Kuí",
"email": "cdrysdall1b@51.la",
"image": "https://robohash.org/utquodeos.png?size=50x50&set=set1"
},
{
"name": "Rébecca",
"email": "ldavall1c@vimeo.com",
"image": "https://robohash.org/delenitiquasiid.png?size=50x50&set=set1"
},
{
"name": "Eléonore",
"email": "brickhuss1d@ustream.tv",
"image": "https://robohash.org/consecteturcommodiiure.png?size=50x50&set=set1"
},
{
"name": "Crééz",
"email": "jaggott1e@is.gd",
"image": "https://robohash.org/minimanisidelectus.png?size=50x50&set=set1"
},
{
"name": "Célia",
"email": "kspuffard1f@ca.gov",
"image": "https://robohash.org/ipsumaliquidcumque.png?size=50x50&set=set1"
},
{
"name": "Marie-thérèse",
"email": "cmaharg1g@psu.edu",
"image": "https://robohash.org/liberoquaerataut.png?size=50x50&set=set1"
},
{
"name": "Publicité",
"email": "ineeds1h@example.com",
"image": "https://robohash.org/aliquidrepudiandaeeum.png?size=50x50&set=set1"
},
{
"name": "Maïlis",
"email": "lalliston1i@miibeian.gov.cn",
"image": "https://robohash.org/estoptioquia.png?size=50x50&set=set1"
},
{
"name": "Bérengère",
"email": "tcullingford1j@squidoo.com",
"image": "https://robohash.org/saepeetvel.png?size=50x50&set=set1"
},
{
"name": "Styrbjörn",
"email": "skalewe1k@archive.org",
"image": "https://robohash.org/sednequevoluptatem.png?size=50x50&set=set1"
},
{
"name": "Bérénice",
"email": "kgoudard1l@dell.com",
"image": "https://robohash.org/necessitatibusvelrerum.png?size=50x50&set=set1"
},
{
"name": "Nuó",
"email": "lpenticost1m@angelfire.com",
"image": "https://robohash.org/autemsitsaepe.png?size=50x50&set=set1"
},
{
"name": "Cloé",
"email": "idomnin1n@bbc.co.uk",
"image": "https://robohash.org/utetut.png?size=50x50&set=set1"
},
{
"name": "Irène",
"email": "ddutchburn1o@npr.org",
"image": "https://robohash.org/etculpased.png?size=50x50&set=set1"
},
{
"name": "Stéphanie",
"email": "achafney1p@dagondesign.com",
"image": "https://robohash.org/laudantiumilloconsequatur.png?size=50x50&set=set1"
},
{
"name": "Eloïse",
"email": "gholttom1q@salon.com",
"image": "https://robohash.org/voluptatemestaut.png?size=50x50&set=set1"
},
{
"name": "Märta",
"email": "agierek1r@rediff.com",
"image": "https://robohash.org/sintcommodiid.png?size=50x50&set=set1"
},
{
"name": "Mélinda",
"email": "dkeoghan1s@java.com",
"image": "https://robohash.org/erroripsumdoloribus.png?size=50x50&set=set1"
},
{
"name": "Adélie",
"email": "bsenior1t@ca.gov",
"image": "https://robohash.org/possimusistelabore.png?size=50x50&set=set1"
},
{
"name": "Célia",
"email": "lstenner1u@drupal.org",
"image": "https://robohash.org/nemonihilquas.png?size=50x50&set=set1"
},
{
"name": "Géraldine",
"email": "mstephens1v@csmonitor.com",
"image": "https://robohash.org/providentillumlibero.png?size=50x50&set=set1"
},
{
"name": "Inès",
"email": "mskacel1w@nationalgeographic.com",
"image": "https://robohash.org/quiutvoluptatem.png?size=50x50&set=set1"
},
{
"name": "Táng",
"email": "swilber1x@reverbnation.com",
"image": "https://robohash.org/rerumrepellatdolor.png?size=50x50&set=set1"
},
{
"name": "Maëlyss",
"email": "ncamings1y@rediff.com",
"image": "https://robohash.org/quamminimanon.png?size=50x50&set=set1"
},
{
"name": "Jú",
"email": "jcard1z@instagram.com",
"image": "https://robohash.org/voluptasinciduntrerum.png?size=50x50&set=set1"
},
{
"name": "Lauréna",
"email": "lstruttman20@msu.edu",
"image": "https://robohash.org/exercitationemquiea.png?size=50x50&set=set1"
},
{
"name": "Nadège",
"email": "mcreber21@accuweather.com",
"image": "https://robohash.org/officiisdelenitia.png?size=50x50&set=set1"
},
{
"name": "Aloïs",
"email": "lsquires22@va.gov",
"image": "https://robohash.org/quiquasdolorum.png?size=50x50&set=set1"
},
{
"name": "Loïs",
"email": "btempest23@taobao.com",
"image": "https://robohash.org/quisminimaaccusantium.png?size=50x50&set=set1"
},
{
"name": "Åke",
"email": "mmullan24@forbes.com",
"image": "https://robohash.org/molestiaerepellenduseos.png?size=50x50&set=set1"
},
{
"name": "Mélia",
"email": "afarnhill25@163.com",
"image": "https://robohash.org/quasdoloresquam.png?size=50x50&set=set1"
},
{
"name": "Maëlyss",
"email": "wpache26@sakura.ne.jp",
"image": "https://robohash.org/doloresintrecusandae.png?size=50x50&set=set1"
},
{
"name": "Loïca",
"email": "tsoal27@taobao.com",
"image": "https://robohash.org/accusamusilloaut.png?size=50x50&set=set1"
},
{
"name": "Léana",
"email": "gparkisson28@jiathis.com",
"image": "https://robohash.org/omnisarchitectoaut.png?size=50x50&set=set1"
},
{
"name": "Anaël",
"email": "nslegg29@canalblog.com",
"image": "https://robohash.org/quiaidadipisci.png?size=50x50&set=set1"
},
{
"name": "Marie-josée",
"email": "salford2a@hubpages.com",
"image": "https://robohash.org/nihilestillo.png?size=50x50&set=set1"
}
]

50
seed-data/index.ts Normal file
View File

@ -0,0 +1,50 @@
import "dotenv/config";
import { db } from "../src/server/db";
import { articles, categories, users } from "../src/server/db/schema";
async function seed() {
const usersData = Array.from({ length: 100 }).map((_, i) => ({
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
createdAt: new Date(Date.now() + i),
}));
const u = await db
.insert(users)
.values(usersData)
.returning({ id: users.id });
console.log("Seeded " + u.length + " users");
const categoriesData = Array.from({ length: 250 }).map((_, i) => ({
name: `Kategorie ${i + 1}`,
slug: `Kategorie-${i + 1}`,
createdAt: new Date(Date.now() + i),
}));
const c = await db.insert(categories).values(categoriesData).returning({
id: categories.id,
});
console.log("Seeded " + c.length + " categories");
const articlesData = Array.from({ length: 500 }).map((_, i) => ({
title: `Artikel ${i + 1}`,
slug: `Artikel-${i + 1}`,
createdAt: new Date(Date.now() + i * 5),
published: true,
}));
const a = await db
.insert(articles)
.values(articlesData)
.returning({ id: articles.id });
console.log("Seeded " + a.length + " articles");
}
async function init() {
try {
await seed();
} catch (error) {
console.error(error);
} finally {
process.exit();
}
}
init();

View File

@ -1,42 +0,0 @@
import "dotenv/config";
import { db, DBType } from "../src/server/db";
import { articles, categories, users } from "../src/server/db/schema";
import fakeArticles from "./fake-articles.json";
import fakeUsers from "./fake-users.json";
import fakeCategories from "./fake-categories.json";
import { generateSlug } from "@/lib/utils";
import { createId } from "@paralleldrive/cuid2";
async function seed() {
const u = await db
.insert(users)
.values(fakeUsers)
.returning({ id: users.id });
console.log("Seeded " + u.length + " users");
const c = await db
.insert(categories)
.values(
fakeCategories.map(({ name }) => ({ name, slug: generateSlug(name) })),
)
.returning({
id: categories.id,
});
console.log("Seeded " + c.length + " categories");
const a = await db
.insert(articles)
.values(
fakeArticles.map(({ title }) => ({
title,
slug: createId(),
published: true,
})),
)
.returning({ id: articles.id });
console.log("Seeded " + a.length + " articles");
}
seed()
.catch(console.error)
.finally(() => process.exit());

View File

@ -1,8 +1,8 @@
"use client";
import React from "react";
import CategoriesGrid, {
CategoriesGridSkeleton,
} from "@/components/category/categories-grid";
import CategoryGrid, {
CategoryGridSkeleton,
} from "@/components/category/grid/category-grid";
import ArticleGrid, {
ArticleGridSkeleton,
} from "@/components/article/grid/article-grid";

View File

@ -1,13 +1,11 @@
import CategoriesGrid from "@/components/category/categories-grid";
import InfiniteCategoryGrid from "@/components/category/grid/infinite-category-grid";
import { api } from "@/trpc/server";
import React from "react";
async function Page() {
const categories = await api.category.getAll();
return (
<>
<h1 className="text-2xl font-bold">Kategorien</h1>
<CategoriesGrid categories={categories} />
<InfiniteCategoryGrid />
</>
);
}

View File

@ -117,6 +117,4 @@ const sortItems: Array<{
{ Icon: CalendarArrowDown, value: "oldest", label: "Älteste" },
{ Icon: ArrowDownAZ, value: "abc", label: "Alphabetisch A-Z" },
{ Icon: ArrowUpAz, value: "cba", label: "Alphabetisch Z-A" },
// { Icon: Eye, value: "popular", label: "Beliebteste" },
// { Icon: MessageSquare, value: "commented", label: "Meistkommentiert" },
];

View File

@ -1,13 +1,14 @@
import CategoryCard from "@/components/category/category-card";
import { Category } from "@/server/db/schema";
import React from "react";
import { Skeleton } from "../ui/skeleton";
import { Skeleton } from "@/components/ui/skeleton";
const GRID_CLASS = "grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3";
export const CATEGORY_GRID_CLASS =
"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3";
function CategoriesGrid({ categories }: { categories: Category[] }) {
function CategoryGrid({ categories }: { categories: Category[] }) {
return (
<menu className={GRID_CLASS}>
<menu className={CATEGORY_GRID_CLASS}>
{categories.map((category) => (
<li key={category.id}>
<CategoryCard {...category} />
@ -17,12 +18,12 @@ function CategoriesGrid({ categories }: { categories: Category[] }) {
);
}
export default CategoriesGrid;
export default CategoryGrid;
export function CategoriesGridSkeleton() {
export function CategoryGridSkeleton() {
const range = Array.from(new Array(6).keys());
return (
<ul className={GRID_CLASS}>
<ul className={CATEGORY_GRID_CLASS}>
{range.map((i) => (
<li key={i}>
<Skeleton className="h-12 w-full" />

View File

@ -0,0 +1,67 @@
"use client";
import { api } from "@/trpc/react";
import React from "react";
import CategoryCard from "../category-card";
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
import { Skeleton } from "@/components/ui/skeleton";
import { CATEGORY_GRID_CLASS } from "./category-grid";
// import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
export default function InfiniteCategoryGrid() {
// const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
// undefined,
// );
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
api.category.getByCursor.useInfiniteQuery(
{
// filter,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
);
// Calculate all visible items across all loaded pages
const allItems = React.useMemo(() => {
return data?.pages.flatMap((page) => page.items) || [];
}, [data]);
// Ref for bottom observation
const bottomObserverRef = React.useRef(null);
useInfiniteItemsObserver({
bottomObserverRef,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
});
return (
<div className="relative space-y-4">
{/* <ArticleFilterBar onFilterUpdate={setFilter} /> */}
<menu className={`${CATEGORY_GRID_CLASS} overflow-auto`}>
{data?.pages?.length
? allItems.map((category, idx) => (
<li key={`category-${idx}`}>
<CategoryCard {...category} />
</li>
))
: null}
{/* Loading indicator */}
{(isLoading || isFetchingNextPage) &&
Array.from(new Array(isLoading ? 16 : 4).keys()).map((idx) => (
<li key={idx}>
<Skeleton className="size-full min-h-20" />
</li>
))}
{/* Bottom observer element */}
{hasNextPage && (
<li ref={bottomObserverRef} className="col-span-full h-12" />
)}
</menu>
</div>
);
}

View File

@ -5,3 +5,8 @@ export const categorySchema = z.object({
description: z.string().optional(),
image: z.string().optional(),
});
export const categoryFilterSchema = z.object({
query: z.string().optional(),
sort: z.string().optional(),
});

View File

@ -11,22 +11,50 @@ import {
articleSchema,
createArticleSchema,
} from "@/lib/validation/zod/article";
import { and, asc, count, desc, eq, gt, ilike, like, sql } from "drizzle-orm";
import {
and,
asc,
count,
desc,
eq,
gte,
ilike,
like,
lte,
sql,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";
type ArticleCursor = Pick<Article, "slug" | "createdAt" | "title">;
const getArticleSorting = (sort: string, cursor?: ArticleCursor) => {
// Default to newest
const baseCase = {
orderBy: [desc(articles.createdAt), asc(articles.slug)],
cursor: cursor ? lte(articles.createdAt, cursor.createdAt) : undefined,
};
const getArticleSorting = (sort: string) => {
switch (sort) {
case "newest":
return desc(articles.createdAt);
return baseCase;
case "oldest":
return asc(articles.createdAt);
return {
orderBy: [asc(articles.createdAt), asc(articles.slug)],
cursor: cursor ? gte(articles.createdAt, cursor.createdAt) : undefined,
};
case "abc":
return asc(articles.title);
return {
orderBy: [asc(articles.title), asc(articles.slug)],
cursor: cursor ? gte(articles.title, cursor.title) : undefined,
};
case "cba":
return desc(articles.title);
return {
orderBy: [desc(articles.title), asc(articles.slug)],
cursor: cursor ? lte(articles.title, cursor.title) : undefined,
};
default:
return desc(articles.createdAt); // Default to newest
return baseCase;
}
};
@ -59,15 +87,32 @@ export const articleRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
const { cursor } = input!;
const limit = input?.limit ?? 50;
const cursorArg = cursor ? gt(articles.slug, cursor) : undefined;
// Decode cursor if using it
let cursorObj: ArticleCursor | undefined;
if (cursor) {
try {
cursorObj = SuperJSON.parse(Buffer.from(cursor, "base64").toString());
} catch (e) {
// Handle invalid cursor
cursorObj = undefined;
}
}
const queryFilterArg = input?.filter?.query?.length
? ilike(articles.title, "%" + input.filter.query + "%")
: undefined;
const categoryArg = input?.filter?.category
? eq(articles.categoryId, input.filter.category)
: undefined;
const orderBy = getArticleSorting(input?.filter?.sort ?? "newest");
const sortConfig = input?.filter?.sort ?? "newest";
const { orderBy, cursor: cursorArg } = getArticleSorting(
sortConfig,
cursorObj,
);
const items = await ctx.db.query.articles.findMany({
where: and(
cursorArg,
@ -83,15 +128,26 @@ export const articleRouter = createTRPCRouter({
createdAt: true,
},
});
let nextCursor: typeof cursor | undefined = undefined;
let nextCursor: string | undefined = undefined;
if (items.length > limit) {
const nextItem = items.pop();
nextCursor = nextItem!.slug;
console.log("Configure next cursor");
const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting
const cursorData: ArticleCursor = {
slug: cursorItem!.slug,
createdAt: cursorItem!.createdAt,
title: cursorItem!.title,
};
// Encode the cursor as base64
nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
"base64",
);
}
return {
items,
nextCursor,
previousCursor: cursor,
};
}),
getAll: publicProcedure
@ -160,10 +216,6 @@ export const articleRouter = createTRPCRouter({
if (!isEditor) {
throw new Error("You are not allowed to update articles");
}
console.log(
"Content before save",
JSON.stringify(input.article.content),
);
return await ctx.db
.update(articles)

View File

@ -6,10 +6,58 @@ import {
} from "@/server/api/trpc";
import { categories, Category } from "@/server/db/schema";
import { count, eq, like } from "drizzle-orm";
import {
and,
asc,
count,
desc,
eq,
gt,
gte,
ilike,
like,
lte,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { categorySchema } from "@/lib/validation/zod/category";
import {
categoryFilterSchema,
categorySchema,
} from "@/lib/validation/zod/category";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";
type CategoryCursor = Pick<Category, "slug" | "createdAt" | "name">;
const getCategorySorting = (sort: string, cursor?: CategoryCursor) => {
// Default to newest
const baseCase = {
orderBy: [desc(categories.createdAt), asc(categories.slug)],
cursor: cursor ? lte(categories.createdAt, cursor.createdAt) : undefined,
};
switch (sort) {
case "newest":
return baseCase;
case "oldest":
return {
orderBy: [asc(categories.createdAt), asc(categories.slug)],
cursor: cursor
? gte(categories.createdAt, cursor.createdAt)
: undefined,
};
case "abc":
return {
orderBy: [asc(categories.name), asc(categories.slug)],
cursor: cursor ? gte(categories.name, cursor.name) : undefined,
};
case "cba":
return {
orderBy: [desc(categories.name), asc(categories.slug)],
cursor: cursor ? lte(categories.name, cursor.name) : undefined,
};
default:
return baseCase;
}
};
export const categoryRouter = createTRPCRouter({
search: publicProcedure
@ -42,6 +90,71 @@ export const categoryRouter = createTRPCRouter({
});
}),
getByCursor: publicProcedure
.input(
z.object({
limit: z.number().optional(),
cursor: z.string().optional(),
filter: categoryFilterSchema.optional(),
}),
)
.query(async ({ ctx, input }) => {
const { cursor } = input!;
const limit = input?.limit ?? 50;
// Decode cursor if using it
let cursorObj: CategoryCursor | undefined;
if (cursor) {
try {
cursorObj = SuperJSON.parse(Buffer.from(cursor, "base64").toString());
} catch (e) {
// Handle invalid cursor
cursorObj = undefined;
}
}
const queryFilterArg = input?.filter?.query?.length
? ilike(categories.name, "%" + input.filter.query + "%")
: undefined;
const sortConfig = input?.filter?.sort ?? "newest";
const { orderBy, cursor: cursorArg } = getCategorySorting(
sortConfig,
cursorObj,
);
const items = (await ctx.db.query.categories.findMany({
where: and(cursorArg, queryFilterArg),
limit: limit + 1,
orderBy,
columns: {
name: true,
slug: true,
createdAt: true,
},
})) as Category[];
let nextCursor: string | undefined = undefined;
if (items.length > limit) {
console.log("Configure next cursor");
const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting
const cursorData: CategoryCursor = {
slug: cursorItem!.slug,
createdAt: cursorItem!.createdAt,
name: cursorItem!.name,
};
// Encode the cursor as base64
nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
"base64",
);
}
return {
items,
nextCursor,
};
}),
getCount: publicProcedure.query(async ({ ctx }) => {
return (await ctx.db.select({ count: count() }).from(categories))[0]?.count;
}),

View File

@ -29,7 +29,7 @@ export const articles = createTable(
.primaryKey()
.$defaultFn(() => createId())
.notNull(),
title: varchar("title", { length: 256 }),
title: varchar("title", { length: 256 }).notNull(),
slug: varchar("slug", { length: 256 }).unique().notNull(),
authorId: varchar("author_id", { length: 255 }),
content: jsonb("content").$type<JSONContent>(),
@ -68,7 +68,7 @@ export const categories = createTable(
.primaryKey()
.$defaultFn(() => createId())
.notNull(),
name: varchar("name", { length: 256 }),
name: varchar("name", { length: 256 }).notNull(),
description: text("description"),
slug: varchar("slug", { length: 256 }).unique().notNull(),