Reading history
This commit is contained in:
@@ -12,6 +12,7 @@ use Illuminate\Contracts\Routing\ResponseFactory;
|
|||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response as IlluminateHttpResponse;
|
use Illuminate\Http\Response as IlluminateHttpResponse;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
@@ -169,9 +170,14 @@ class ComicController extends Controller
|
|||||||
// Do an upsert
|
// Do an upsert
|
||||||
Chapter::upsert($arrayForUpsert, uniqueBy: 'chapter_uuid');
|
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', [
|
return Inertia::render('Comic/Chapters', [
|
||||||
'comic' => $comic,
|
'comic' => $comic,
|
||||||
'chapters' => $chapters,
|
'chapters' => $chapters,
|
||||||
|
'histories' => $histories
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,12 +219,32 @@ class ComicController extends Controller
|
|||||||
// Do an upsert
|
// Do an upsert
|
||||||
Image::upsert($arrayForUpsert, uniqueBy: 'url');
|
Image::upsert($arrayForUpsert, uniqueBy: 'url');
|
||||||
|
|
||||||
|
// Update history
|
||||||
|
$request->user()->readingHistories()->attach($chapterObj->id, ['comic_id' => $comicObj->id]);
|
||||||
|
|
||||||
return Inertia::render('Comic/Read', [
|
return Inertia::render('Comic/Read', [
|
||||||
'comic' => $comic,
|
'comic' => $comic,
|
||||||
'chapter' => $chapter,
|
'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()
|
public function tags()
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
|
|
||||||
public function readingHistories(): BelongsToMany
|
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-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
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 { Separator } from "@radix-ui/react-separator";
|
||||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from "@/components/ui/breadcrumb";
|
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from "@/components/ui/breadcrumb";
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Head, Link, router } from '@inertiajs/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 AppLayout from '@/Layouts/AppLayout.jsx';
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb';
|
import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
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 [group, setGroup] = useState('default');
|
||||||
const [favourites, setFavourites] = useState(auth.user.favourites);
|
const [favourites, setFavourites] = useState(auth.user.favourites);
|
||||||
@@ -42,12 +42,13 @@ export default function Chapters({ auth, comic, chapters }) {
|
|||||||
|
|
||||||
const ComicChapterLink = (props) => {
|
const ComicChapterLink = (props) => {
|
||||||
const isNew = Date.now() - Date.parse(props.datetime_created) < 6.048e+8;
|
const isNew = Date.now() - Date.parse(props.datetime_created) < 6.048e+8;
|
||||||
|
const isRead = histories.includes(props.uuid);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<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 }` }>
|
<Link className="relative" href={ `/comic/${ comic.comic.path_word }/${ props.uuid }` }>
|
||||||
{ props.name }
|
{ props.name }
|
||||||
{ isNew && <Plus size={ 16 } className="text-xs absolute right-0 top-0" /> }
|
{ 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) => {
|
const toggleAscending = (e) => {
|
||||||
setAscending(!ascending);
|
setAscending(!ascending);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ export default function Favourites({ auth, favourites }) {
|
|||||||
const favouriteOnClickHandler = (pathword) => {
|
const favouriteOnClickHandler = (pathword) => {
|
||||||
axios.post(route('comics.postFavourite'), { pathword: pathword }).then(res => {
|
axios.post(route('comics.postFavourite'), { pathword: pathword }).then(res => {
|
||||||
setStateFavourites(stateFavourites.filter(f => f.pathword !== pathword));
|
setStateFavourites(stateFavourites.filter(f => f.pathword !== pathword));
|
||||||
console.log(stateFavourites);
|
|
||||||
//setFavourites(res.data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
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>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem asChild><Link href={ route('profile.edit') }><BadgeCheck /> Profile</Link></DropdownMenuItem>
|
<DropdownMenuItem asChild><Link href={ route('profile.edit') }><BadgeCheck /> Profile</Link></DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild><Link href={ route('comics.favourites') }><Star /> Favourites</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>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild><Link method="post" href={ route('logout') }><LogOut /> Log out</Link></DropdownMenuItem>
|
<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\ComicController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use Illuminate\Foundation\Application;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
@@ -21,6 +20,10 @@ Route::controller(ComicController::class)->middleware('auth')->name('comics.')->
|
|||||||
|
|
||||||
// Toggle favourites
|
// Toggle favourites
|
||||||
Route::post('/favourites', 'postFavourite')->name('postFavourite');
|
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 () {
|
Route::get('/dashboard', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user