feat: Implement news listing page with search, filter, and pagination, and update news table schema.
This commit is contained in:
parent
2eaed7bb27
commit
861aff3c78
70
app/Http/Controllers/Home/NewsController.php
Normal file
70
app/Http/Controllers/Home/NewsController.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Home;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\News;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class NewsController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$search = $request->get('search');
|
||||||
|
$category = $request->get('category');
|
||||||
|
|
||||||
|
$news = News::with(['author', 'categories'])
|
||||||
|
->published()
|
||||||
|
->when($search, function ($query, $search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('title', 'like', "%{$search}%")
|
||||||
|
->orWhere('content', 'like', "%{$search}%")
|
||||||
|
->orWhere('excerpt', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($category, function ($query, $category) {
|
||||||
|
if ($category !== 'all') {
|
||||||
|
$query->whereHas('categories', function ($q) use ($category) {
|
||||||
|
$q->where('categories.id', $category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->orderBy('published_at', 'desc')
|
||||||
|
->paginate(12)
|
||||||
|
->withQueryString()
|
||||||
|
->through(function ($news) {
|
||||||
|
$news->thumbnail = $news->getFirstMediaUrl('news');
|
||||||
|
$news->formatted_published_at = Carbon::parse($news->published_at)
|
||||||
|
->locale('id')
|
||||||
|
->translatedFormat('l, d F Y');
|
||||||
|
|
||||||
|
return $news;
|
||||||
|
});
|
||||||
|
|
||||||
|
$mostViewedNews = News::with(['author', 'categories'])
|
||||||
|
->published()
|
||||||
|
->orderBy('views', 'desc')
|
||||||
|
->take(4)
|
||||||
|
->get()
|
||||||
|
->map(function ($news) {
|
||||||
|
$news->thumbnail = $news->getFirstMediaUrl('news');
|
||||||
|
$news->formatted_published_at = Carbon::parse($news->published_at)
|
||||||
|
->locale('id')
|
||||||
|
->translatedFormat('l, d F Y');
|
||||||
|
|
||||||
|
return $news;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('home/news/pages/Index', [
|
||||||
|
'pageTitle' => 'Berita',
|
||||||
|
'pageDescription' => 'Dapatkan informasi terbaru, artikel, dan update berita terkini tentang Purwakarta.',
|
||||||
|
'categories' => Category::active()->get(),
|
||||||
|
'news' => $news,
|
||||||
|
'mostViewedNews' => $mostViewedNews,
|
||||||
|
'filters' => request()->only(['search', 'category']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ public function up(): void
|
|||||||
$table->text('content');
|
$table->text('content');
|
||||||
$table->text('excerpt');
|
$table->text('excerpt');
|
||||||
$table->string('link', 50)->nullable();
|
$table->string('link', 50)->nullable();
|
||||||
$table->unsignedInteger('view')->default(0);
|
$table->unsignedInteger('views')->default(0);
|
||||||
$table->enum('status', [NewsStatus::values()])->comment(NewsStatus::comment());
|
$table->enum('status', [NewsStatus::values()])->comment(NewsStatus::comment());
|
||||||
$table->timestamp('published_at')->nullable();
|
$table->timestamp('published_at')->nullable();
|
||||||
$table->timestamp('created_at')->useCurrent();
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
|||||||
43
resources/js/pages/home/components/Headline.jsx
Normal file
43
resources/js/pages/home/components/Headline.jsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default function Headline({ pageTitle, pageDescription }) {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Container
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
spacing={2}
|
||||||
|
useFlexGap
|
||||||
|
sx={{ alignItems: 'center', width: { xs: '100%', sm: '70%' } }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 'clamp(3rem, 10vw, 3.5rem)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pageTitle}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'text.secondary',
|
||||||
|
width: { sm: '100%', md: '80%' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pageDescription}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import MenuIcon from '@mui/icons-material/Menu';
|
|||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
import ColorModeIconDropdown from '../../theme/ColorModeIconDropdown';
|
import ColorModeIconDropdown from '../../theme/ColorModeIconDropdown';
|
||||||
import Logo from './Logo';
|
import Logo from './Logo';
|
||||||
|
import { router } from '@inertiajs/react';
|
||||||
|
|
||||||
const StyledToolbar = styled(Toolbar)(({ theme }) => ({
|
const StyledToolbar = styled(Toolbar)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -53,8 +54,13 @@ export default function Navbar() {
|
|||||||
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', px: 0 }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', px: 0 }}>
|
||||||
<Logo />
|
<Logo />
|
||||||
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||||
<Button variant="text" color="info" size="small">
|
<Button
|
||||||
Features
|
variant="text"
|
||||||
|
color="info"
|
||||||
|
size="small"
|
||||||
|
onClick={() => router.get('/news')}
|
||||||
|
>
|
||||||
|
Berita
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="text" color="info" size="small">
|
<Button variant="text" color="info" size="small">
|
||||||
Testimonials
|
Testimonials
|
||||||
|
|||||||
33
resources/js/pages/home/news/components/Author.jsx
Normal file
33
resources/js/pages/home/news/components/Author.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Avatar, Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export default function Author({ author, publishedAt }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
alt={author}
|
||||||
|
sx={{ width: 24, height: 24 }}
|
||||||
|
>
|
||||||
|
{author.charAt(0).toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{author}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{publishedAt}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
272
resources/js/pages/home/news/components/MainContent.jsx
Normal file
272
resources/js/pages/home/news/components/MainContent.jsx
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import { router } from '@inertiajs/react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import CardMedia from '@mui/material/CardMedia';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Headline from '../../components/Headline';
|
||||||
|
import { Pagination } from '@mui/material';
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import Author from './Author';
|
||||||
|
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
|
||||||
|
import { Search } from './Search';
|
||||||
|
|
||||||
|
const StyledCard = styled(Card)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: 0,
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: (theme.vars || theme).palette.background.paper,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: '3px solid',
|
||||||
|
outlineColor: 'hsla(210, 98%, 48%, 0.5)',
|
||||||
|
outlineOffset: '2px',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCardContent = styled(CardContent)({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
padding: 16,
|
||||||
|
flexGrow: 1,
|
||||||
|
'&:last-child': {
|
||||||
|
paddingBottom: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledTypography = styled(Typography)({
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function MainContent({ pageTitle, pageDescription, categories, news, filters }) {
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState(filters?.category || 'all');
|
||||||
|
const [search, setSearch] = useState(filters?.search || '');
|
||||||
|
const [focusedCardIndex, setFocusedCardIndex] = useState(null);
|
||||||
|
const searchDebounceRef = useRef(null);
|
||||||
|
|
||||||
|
const handleFocus = (index) => {
|
||||||
|
setFocusedCardIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (search === (filters?.search || '')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchDebounceRef.current) {
|
||||||
|
clearTimeout(searchDebounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchDebounceRef.current = setTimeout(() => {
|
||||||
|
router.get(window.location.pathname, {
|
||||||
|
category: selectedCategory,
|
||||||
|
search: search
|
||||||
|
}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (searchDebounceRef.current) {
|
||||||
|
clearTimeout(searchDebounceRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setFocusedCardIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCategory = (category) => {
|
||||||
|
if (searchDebounceRef.current) {
|
||||||
|
clearTimeout(searchDebounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCategory(category);
|
||||||
|
router.get(window.location.pathname, {
|
||||||
|
category: category,
|
||||||
|
search: search
|
||||||
|
}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (searchDebounceRef.current) {
|
||||||
|
clearTimeout(searchDebounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get(window.location.pathname, {
|
||||||
|
category: selectedCategory,
|
||||||
|
search: search
|
||||||
|
}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (event, value) => {
|
||||||
|
router.get(window.location.pathname, {
|
||||||
|
page: value,
|
||||||
|
category: selectedCategory,
|
||||||
|
search: search
|
||||||
|
}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
<div>
|
||||||
|
<Headline pageTitle={pageTitle} pageDescription={pageDescription} />
|
||||||
|
</div>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'flex', sm: 'none' },
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 1,
|
||||||
|
width: { xs: '100%', md: 'fit-content' },
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search value={search} onChange={(e) => setSearch(e.target.value)} onSearch={handleSearch} />
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column-reverse', md: 'row' },
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: { xs: 'start', md: 'center' },
|
||||||
|
gap: 4,
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
'&::-webkit-scrollbar': { display: 'none' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
onClick={() => handleCategory('all')}
|
||||||
|
size="medium"
|
||||||
|
label="Semua"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: selectedCategory === 'all' ? '' : 'transparent',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{categories.map((category, index) =>
|
||||||
|
<Chip
|
||||||
|
onClick={() => handleCategory(category.id)}
|
||||||
|
size="medium"
|
||||||
|
label={category.name}
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: selectedCategory === category.id ? '' : 'transparent',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', sm: 'flex' },
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 1,
|
||||||
|
width: { xs: '100%', md: 'fit-content' },
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search value={search} onChange={(e) => setSearch(e.target.value)} onSearch={handleSearch} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={2} columns={12}>
|
||||||
|
{news.data.map((news, index) =>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }} key={index}>
|
||||||
|
<StyledCard
|
||||||
|
variant="outlined"
|
||||||
|
onFocus={() => handleFocus(news.id)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
tabIndex={0}
|
||||||
|
className={focusedCardIndex === news.id ? 'Mui-focused' : ''}
|
||||||
|
>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
alt={news.title}
|
||||||
|
image={news.thumbnail}
|
||||||
|
sx={{
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<StyledCardContent>
|
||||||
|
<Typography gutterBottom variant="caption" component="div">
|
||||||
|
{news.categories.map((category, index) =>
|
||||||
|
<Chip label={category.name} color="success" variant="outlined" sx={{ mr: 1 }} key={index} />
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Typography gutterBottom variant="h6" component="div">
|
||||||
|
{news.title}
|
||||||
|
</Typography>
|
||||||
|
<StyledTypography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
gutterBottom
|
||||||
|
dangerouslySetInnerHTML={{ __html: news.excerpt }}
|
||||||
|
/>
|
||||||
|
</StyledCardContent>
|
||||||
|
<Author author={news.author?.name} publishedAt={news.formatted_published_at} />
|
||||||
|
</StyledCard>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{news.data.length > 0 ? (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'row', py: 4 }}>
|
||||||
|
<Pagination
|
||||||
|
count={news.last_page}
|
||||||
|
showFirstButton
|
||||||
|
showLastButton
|
||||||
|
page={news.current_page}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column' }}>
|
||||||
|
<DotLottieReact
|
||||||
|
src="/assets/lottie/cat-fishing.lottie"
|
||||||
|
loop
|
||||||
|
autoplay
|
||||||
|
style={{ width: '240px', height: '240px' }}
|
||||||
|
/>
|
||||||
|
<Typography gutterBottom variant="body1" component="div">
|
||||||
|
Tidak ada berita yang ditemukan
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Box >
|
||||||
|
);
|
||||||
|
}
|
||||||
114
resources/js/pages/home/news/components/MostViewed.jsx
Normal file
114
resources/js/pages/home/news/components/MostViewed.jsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import NavigateNextRoundedIcon from '@mui/icons-material/NavigateNextRounded';
|
||||||
|
import { Chip } from '@mui/material';
|
||||||
|
import Author from './Author';
|
||||||
|
|
||||||
|
const StyledTypography = styled(Typography)({
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
});
|
||||||
|
|
||||||
|
const TitleTypography = styled(Typography)(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': { cursor: 'pointer' },
|
||||||
|
'& .arrow': {
|
||||||
|
visibility: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
},
|
||||||
|
'&:hover .arrow': {
|
||||||
|
visibility: 'visible',
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: '3px solid',
|
||||||
|
outlineColor: 'hsla(210, 98%, 48%, 0.5)',
|
||||||
|
outlineOffset: '3px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
width: 0,
|
||||||
|
height: '1px',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
backgroundColor: (theme.vars || theme).palette.text.primary,
|
||||||
|
opacity: 0.3,
|
||||||
|
transition: 'width 0.3s ease, opacity 0.3s ease',
|
||||||
|
},
|
||||||
|
'&:hover::before': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function MostViewed({ mostViewedNews }) {
|
||||||
|
const [focusedCardIndex, setFocusedCardIndex] = React.useState(null);
|
||||||
|
|
||||||
|
const handleFocus = (index) => {
|
||||||
|
setFocusedCardIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setFocusedCardIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography variant="h2" gutterBottom>
|
||||||
|
Paling Banyak Dilihat
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={8} columns={12} sx={{ my: 4 }}>
|
||||||
|
{mostViewedNews.map((news, index) => (
|
||||||
|
<Grid key={index} size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 1,
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography gutterBottom variant="caption" component="div">
|
||||||
|
{news.categories.map((category, index) =>
|
||||||
|
<Chip label={category.name} color="success" variant="outlined" sx={{ mr: 1 }} key={index} />
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<TitleTypography
|
||||||
|
gutterBottom
|
||||||
|
variant="h6"
|
||||||
|
onFocus={() => handleFocus(index)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
tabIndex={0}
|
||||||
|
className={focusedCardIndex === index ? 'Mui-focused' : ''}
|
||||||
|
>
|
||||||
|
{news.title}
|
||||||
|
<NavigateNextRoundedIcon
|
||||||
|
className="arrow"
|
||||||
|
sx={{ fontSize: '1rem' }}
|
||||||
|
/>
|
||||||
|
</TitleTypography>
|
||||||
|
|
||||||
|
<StyledTypography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
{news.description}
|
||||||
|
</StyledTypography>
|
||||||
|
|
||||||
|
<Author author={news.author?.name} publishedAt={news.formatted_published_at} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
resources/js/pages/home/news/components/Search.jsx
Normal file
31
resources/js/pages/home/news/components/Search.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { FormControl, InputAdornment, OutlinedInput } from "@mui/material";
|
||||||
|
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
|
||||||
|
|
||||||
|
export function Search({ value, onChange, onSearch }) {
|
||||||
|
return (
|
||||||
|
<FormControl sx={{ width: { xs: '100%', md: '100ch' } }} variant="outlined">
|
||||||
|
<OutlinedInput
|
||||||
|
autoComplete='off'
|
||||||
|
size="small"
|
||||||
|
id="search"
|
||||||
|
placeholder="Cari berita..."
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startAdornment={
|
||||||
|
<InputAdornment position="start" sx={{ color: 'text.primary' }}>
|
||||||
|
<SearchRoundedIcon fontSize="small" />
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
'aria-label': 'search',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
resources/js/pages/home/news/pages/Index.jsx
Normal file
32
resources/js/pages/home/news/pages/Index.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import AppTheme from '../../../theme/AppTheme';
|
||||||
|
import Navbar from '../../components/Navbar';
|
||||||
|
import MainContent from '../components/MainContent';
|
||||||
|
import MostViewed from '../components/MostViewed';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
|
||||||
|
export default function NewsIndexPage(props) {
|
||||||
|
return (
|
||||||
|
<AppTheme {...props}>
|
||||||
|
<CssBaseline enableColorScheme />
|
||||||
|
|
||||||
|
<Navbar />
|
||||||
|
<Container
|
||||||
|
maxWidth="lg"
|
||||||
|
component="main"
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', my: 16, gap: 4 }}
|
||||||
|
>
|
||||||
|
<MainContent
|
||||||
|
pageTitle={props.pageTitle}
|
||||||
|
pageDescription={props.pageDescription}
|
||||||
|
categories={props.categories}
|
||||||
|
news={props.news}
|
||||||
|
filters={props.filters}
|
||||||
|
/>
|
||||||
|
<MostViewed mostViewedNews={props.mostViewedNews} />
|
||||||
|
</Container>
|
||||||
|
<Footer />
|
||||||
|
</AppTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Home\HomepageController;
|
use App\Http\Controllers\Home\HomepageController;
|
||||||
|
use App\Http\Controllers\Home\NewsController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', [HomepageController::class, 'index'])->name('homepage');
|
Route::get('/', [HomepageController::class, 'index'])->name('homepage');
|
||||||
|
Route::get('/news', [NewsController::class, 'index'])->name('news');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user