Reading history
This commit is contained in:
@@ -12,6 +12,7 @@ use Illuminate\Contracts\Routing\ResponseFactory;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response as IlluminateHttpResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -169,9 +170,14 @@ class ComicController extends Controller
|
||||
// Do an upsert
|
||||
Chapter::upsert($arrayForUpsert, uniqueBy: 'chapter_uuid');
|
||||
|
||||
// Get history
|
||||
$histories = $request->user()->readingHistories()->where('reading_histories.comic_id', $comicObject->id)
|
||||
->distinct()->select('chapter_uuid')->get()->pluck('chapter_uuid');
|
||||
|
||||
return Inertia::render('Comic/Chapters', [
|
||||
'comic' => $comic,
|
||||
'chapters' => $chapters,
|
||||
'histories' => $histories
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -213,12 +219,32 @@ class ComicController extends Controller
|
||||
// Do an upsert
|
||||
Image::upsert($arrayForUpsert, uniqueBy: 'url');
|
||||
|
||||
// Update history
|
||||
$request->user()->readingHistories()->attach($chapterObj->id, ['comic_id' => $comicObj->id]);
|
||||
|
||||
return Inertia::render('Comic/Read', [
|
||||
'comic' => $comic,
|
||||
'chapter' => $chapter,
|
||||
]);
|
||||
}
|
||||
|
||||
public function histories(Request $request): Response
|
||||
{
|
||||
// Get history
|
||||
$histories = $request->user()->readingHistories()->with(['comic:id,name,pathword'])->orderByDesc('reading_histories.created_at')
|
||||
->select(['reading_histories.id as hid', 'reading_histories.created_at', 'chapters.comic_id', 'chapters.name'])->paginate(50)->toArray();
|
||||
|
||||
return Inertia::render('Comic/Histories', [
|
||||
'histories' => $histories
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroyHistories(Request $request): RedirectResponse
|
||||
{
|
||||
$histories = $request->user()->readingHistories()->whereIn('reading_histories.id', $request->get('ids'))->delete();
|
||||
return redirect()->route('comics.histories');
|
||||
}
|
||||
|
||||
public function tags()
|
||||
{
|
||||
// TODO
|
||||
|
||||
@@ -56,7 +56,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
|
||||
public function readingHistories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Comic::class, 'reading_histories')->withTimestamps();
|
||||
return $this->belongsToMany(Chapter::class, 'reading_histories')->withTimestamps();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/Components/ui/app-sidebar.jsx';
|
||||
import { AppSidebar } from '@/components/ui/app-sidebar.jsx';
|
||||
import { Separator } from "@radix-ui/react-separator";
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from "@/components/ui/breadcrumb";
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import { Moon, Plus, Star, ArrowDownNarrowWide, ArrowUpNarrowWide } from 'lucide-react';
|
||||
import { Plus, Star, ArrowDownNarrowWide, ArrowUpNarrowWide } from 'lucide-react';
|
||||
|
||||
import AppLayout from '@/Layouts/AppLayout.jsx';
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Badge } from "@/components/ui/badge.jsx";
|
||||
|
||||
export default function Chapters({ auth, comic, chapters }) {
|
||||
export default function Chapters({ auth, comic, chapters, histories }) {
|
||||
|
||||
const [group, setGroup] = useState('default');
|
||||
const [favourites, setFavourites] = useState(auth.user.favourites);
|
||||
@@ -42,12 +42,13 @@ export default function Chapters({ auth, comic, chapters }) {
|
||||
|
||||
const ComicChapterLink = (props) => {
|
||||
const isNew = Date.now() - Date.parse(props.datetime_created) < 6.048e+8;
|
||||
const isRead = histories.includes(props.uuid);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button className="" size="sm" variant="outline" asChild>
|
||||
<Button size="sm" variant="outline" asChild className={ isRead ? 'bg-gray-200 hover:bg-gray-300' : '' }>
|
||||
<Link className="relative" href={ `/comic/${ comic.comic.path_word }/${ props.uuid }` }>
|
||||
{ props.name }
|
||||
{ isNew && <Plus size={ 16 } className="text-xs absolute right-0 top-0" /> }
|
||||
@@ -64,7 +65,6 @@ export default function Chapters({ auth, comic, chapters }) {
|
||||
|
||||
const toggleAscending = (e) => {
|
||||
setAscending(!ascending);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,8 +17,6 @@ export default function Favourites({ auth, favourites }) {
|
||||
const favouriteOnClickHandler = (pathword) => {
|
||||
axios.post(route('comics.postFavourite'), { pathword: pathword }).then(res => {
|
||||
setStateFavourites(stateFavourites.filter(f => f.pathword !== pathword));
|
||||
console.log(stateFavourites);
|
||||
//setFavourites(res.data);
|
||||
});
|
||||
|
||||
toast({
|
||||
|
||||
116
resources/js/Pages/Comic/Histories.jsx
Normal file
116
resources/js/Pages/Comic/Histories.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import AppLayout from '@/Layouts/AppLayout.jsx';
|
||||
|
||||
import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Checkbox from "@/components/Checkbox";
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
import { useToast } from "@/hooks/use-toast.js";
|
||||
|
||||
export default function Histories({ auth, histories }) {
|
||||
|
||||
const { toast } = useToast();
|
||||
const [ids, setIds] = useState([]);
|
||||
|
||||
const checkboxOnChangeHandler = (e) => {
|
||||
if (e.target.checked) {
|
||||
// Add to ids
|
||||
setIds([...ids, e.target.dataset.hid]);
|
||||
} else {
|
||||
setIds(ids.filter(id => id !== e.target.dataset.hid));
|
||||
}
|
||||
}
|
||||
|
||||
const deleteButtonOnClickHandler = (e) => {
|
||||
if (ids.length === 0) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `No items selected.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
router.visit(route('comics.destroyHistories'), {
|
||||
data: { ids: ids },
|
||||
method: "PATCH",
|
||||
only: ['histories'],
|
||||
onSuccess: data => {
|
||||
toast({
|
||||
title: "All set",
|
||||
description: `The histories has been deleted.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout auth={ auth } header={
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Histories</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
}>
|
||||
<Head>
|
||||
<title>Histories</title>
|
||||
</Head>
|
||||
<div className="p-3 pt-1 w-[90%] mx-auto">
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" onClick={ () => deleteButtonOnClickHandler() }>Delete Selected</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Select</TableHead>
|
||||
<TableHead>Comic</TableHead>
|
||||
<TableHead>Chapter</TableHead>
|
||||
<TableHead>Read at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ histories.data.map((h, i) => (
|
||||
<TableRow key={ i }>
|
||||
<TableCell>
|
||||
<label>
|
||||
<Checkbox data-hid={ h.hid } defaultChecked={ false }
|
||||
name={ `checkbox-${ h.hid }` }
|
||||
onChange={ (e) => checkboxOnChangeHandler(e) } />
|
||||
</label>
|
||||
</TableCell>
|
||||
<TableCell>{ h.name }</TableCell>
|
||||
<TableCell>{ h.comic.name }</TableCell>
|
||||
<TableCell>{ h.created_at }</TableCell>
|
||||
</TableRow>
|
||||
)) }
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div>
|
||||
<Pagination className="justify-end">
|
||||
<PaginationContent>
|
||||
{ histories.current_page > 1 && (
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href={ histories.prev_page_url } only={['histories']} />
|
||||
</PaginationItem>
|
||||
) }
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">{ histories.current_page }</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
{ histories.current_page < histories.last_page && (
|
||||
<PaginationItem>
|
||||
<PaginationNext href={ histories.next_page_url } only={['histories']} />
|
||||
</PaginationItem>
|
||||
) }
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
|
||||
type Theme = "dark" | "light" | "system"
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
}
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
root.classList.add(systemTheme)
|
||||
return
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
console.log(storageKey)
|
||||
localStorage.setItem(storageKey, theme)
|
||||
setTheme(theme)
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={ value }>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext)
|
||||
console.log(context);
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export function AppSidebar({ auth }) {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild><Link href={ route('profile.edit') }><BadgeCheck /> Profile</Link></DropdownMenuItem>
|
||||
<DropdownMenuItem asChild><Link href={ route('comics.favourites') }><Star /> Favourites</Link></DropdownMenuItem>
|
||||
<DropdownMenuItem><History /> History</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild><Link href={ route('comics.histories') }><History /> History</Link></DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild><Link method="post" href={ route('logout') }><LogOut /> Log out</Link></DropdownMenuItem>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use App\Http\Controllers\ComicController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -21,6 +20,10 @@ Route::controller(ComicController::class)->middleware('auth')->name('comics.')->
|
||||
|
||||
// Toggle favourites
|
||||
Route::post('/favourites', 'postFavourite')->name('postFavourite');
|
||||
|
||||
// Histories
|
||||
Route::get('/histories', 'histories')->name('histories');
|
||||
Route::patch('/histories', 'destroyHistories')->name('destroyHistories'); // Only patch accept params
|
||||
});
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
|
||||
Reference in New Issue
Block a user