Done
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import MainLayout from '@/layouts/MainLayout.vue';
|
||||
import PageCard from '@/components/PageCard.vue';
|
||||
import SectionHeading from '@/components/SectionHeading.vue';
|
||||
|
||||
defineOptions({ layout: MainLayout });
|
||||
|
||||
defineProps<{
|
||||
status?: string;
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('login.store'), {
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Login" />
|
||||
|
||||
<VContainer class="d-flex justify-center py-8">
|
||||
<PageCard max-width="400">
|
||||
<template #header>
|
||||
<SectionHeading
|
||||
title="Sign in"
|
||||
subtitle="Welcome back. Enter your details to continue reading."
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VAlert
|
||||
v-if="status"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ status }}
|
||||
</VAlert>
|
||||
|
||||
<VForm @submit.prevent="submit">
|
||||
<VTextField
|
||||
v-model="form.email"
|
||||
label="Email address"
|
||||
type="email"
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
:error-messages="form.errors.email"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VTextField
|
||||
v-model="form.password"
|
||||
label="Password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:error-messages="form.errors.password"
|
||||
class="mb-1"
|
||||
/>
|
||||
|
||||
<VCheckbox
|
||||
v-model="form.remember"
|
||||
label="Keep me signed in"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
type="submit"
|
||||
block
|
||||
size="large"
|
||||
color="primary"
|
||||
:loading="form.processing"
|
||||
>
|
||||
Sign in
|
||||
</VBtn>
|
||||
</VForm>
|
||||
|
||||
<template #footer>
|
||||
New to DredgeNews?
|
||||
<Link
|
||||
:href="route('register')"
|
||||
class="text-primary font-weight-medium"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
</template>
|
||||
</PageCard>
|
||||
</VContainer>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import MainLayout from '@/layouts/MainLayout.vue';
|
||||
import PageCard from '@/components/PageCard.vue';
|
||||
import SectionHeading from '@/components/SectionHeading.vue';
|
||||
|
||||
defineOptions({ layout: MainLayout });
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('register.store'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Register" />
|
||||
|
||||
<VContainer class="d-flex justify-center py-8">
|
||||
<PageCard max-width="400">
|
||||
<template #header>
|
||||
<SectionHeading
|
||||
eyebrow="Join DredgeNews"
|
||||
title="Create your account"
|
||||
subtitle="Unlimited stories, straight reporting, no noise."
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VForm @submit.prevent="submit">
|
||||
<VTextField
|
||||
v-model="form.name"
|
||||
label="Full name"
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
:error-messages="form.errors.name"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VTextField
|
||||
v-model="form.email"
|
||||
label="Email address"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
:error-messages="form.errors.email"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VTextField
|
||||
v-model="form.password"
|
||||
label="Password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:error-messages="form.errors.password"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VTextField
|
||||
v-model="form.password_confirmation"
|
||||
label="Confirm password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:error-messages="form.errors.password_confirmation"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
type="submit"
|
||||
block
|
||||
size="large"
|
||||
color="primary"
|
||||
:loading="form.processing"
|
||||
>
|
||||
Create account
|
||||
</VBtn>
|
||||
</VForm>
|
||||
|
||||
<template #footer>
|
||||
Already have an account?
|
||||
<Link
|
||||
:href="route('login')"
|
||||
class="text-primary font-weight-medium"
|
||||
>Sign in</Link
|
||||
>
|
||||
</template>
|
||||
</PageCard>
|
||||
</VContainer>
|
||||
</template>
|
||||
+14
-252
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import MainLayout from '@/layouts/MainLayout.vue';
|
||||
import CategorySection from '@/components/CategorySection.vue';
|
||||
|
||||
defineOptions({
|
||||
layout: MainLayout,
|
||||
@@ -7,9 +8,11 @@ defineOptions({
|
||||
|
||||
interface Story {
|
||||
source: string;
|
||||
time: string;
|
||||
time: string | null;
|
||||
headline: string;
|
||||
deck?: string;
|
||||
deck?: string | null;
|
||||
image?: string | null;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
@@ -18,152 +21,19 @@ interface Category {
|
||||
items: Story[];
|
||||
}
|
||||
|
||||
const categories: Category[] = [
|
||||
{
|
||||
label: 'Headlines',
|
||||
hero: true,
|
||||
items: [
|
||||
{ source: 'Reuters', time: '42 minutes ago', headline: 'Leaders gather in Brussels as ceasefire talks enter critical third day', deck: 'Negotiators signalled cautious progress overnight, with both delegations agreeing to extend the informal moratorium on strikes.' },
|
||||
{ source: 'Financial Times', time: '1 hour ago', headline: 'Markets edge higher on softer-than-expected inflation print', deck: 'Central banks signal caution as core inflation falls for the second consecutive month across G7 economies.' },
|
||||
{ source: 'BBC News', time: '30 minutes ago', headline: 'Record heat across southern Europe for third week running' },
|
||||
{ source: 'AP News', time: '55 minutes ago', headline: 'Congress reaches last-minute deal to avert government shutdown' },
|
||||
{ source: 'The Guardian', time: '1 hour ago', headline: 'Tech giants face fresh scrutiny over data sharing practices' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Australia',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'ABC News', time: '8 minutes ago', headline: 'Treasurer signals pre-election budget revision in August' },
|
||||
{ source: 'Sydney Morning Herald', time: '1 hour ago', headline: 'East coast housing approvals fall for third consecutive quarter' },
|
||||
{ source: 'The Australian', time: '3 hours ago', headline: 'Queensland floods ease but thousands remain displaced' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'UK',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'The Guardian', time: '20 minutes ago', headline: 'Prime Minister faces backbench revolt over public sector pay offer' },
|
||||
{ source: 'BBC News', time: '1 hour ago', headline: 'London rents rise for twelfth consecutive month as supply tightens' },
|
||||
{ source: 'The Times', time: '2 hours ago', headline: 'NHS waiting lists fall slightly but remain at historic highs' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'USA',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'AP News', time: '15 minutes ago', headline: 'Congress reaches last-minute deal to avert government shutdown' },
|
||||
{ source: 'Washington Post', time: '50 minutes ago', headline: 'Federal Reserve signals caution on further rate adjustments' },
|
||||
{ source: 'New York Times', time: '2 hours ago', headline: 'Midwest storms leave hundreds of thousands without power' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Politics',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'Politico', time: '18 minutes ago', headline: 'Senate committee advances sweeping electoral reform bill' },
|
||||
{ source: 'The Hill', time: '55 minutes ago', headline: 'Opposition leader calls for early election amid polling slump' },
|
||||
{ source: 'AP News', time: '2 hours ago', headline: 'Foreign ministers meet in Geneva over disputed border region' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Climate',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'BBC News', time: '30 minutes ago', headline: 'Record heat across southern Europe for third week running' },
|
||||
{ source: 'The Guardian', time: '1 hour ago', headline: 'Arctic sea ice extent hits lowest July measurement on record' },
|
||||
{ source: 'Nature', time: '4 hours ago', headline: 'New modelling suggests 2°C target may be breached by 2031' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Health',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'Reuters', time: '25 minutes ago', headline: 'WHO recommends updated booster schedule ahead of winter' },
|
||||
{ source: 'New Scientist', time: '2 hours ago', headline: "Trial drug shows promise in slowing early-onset Alzheimer's" },
|
||||
{ source: 'The Lancet', time: '5 hours ago', headline: 'Childhood obesity rates plateau in high-income countries for first time' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Sport',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'The Guardian', time: '10 minutes ago', headline: 'Australian side advances after dramatic penalty shootout' },
|
||||
{ source: 'ESPN', time: '1 hour ago', headline: 'Transfer window: three clubs circle record-breaking midfielder' },
|
||||
{ source: 'Reuters', time: '3 hours ago', headline: 'Athletics world championships draw record television audience' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Arts',
|
||||
hero: false,
|
||||
items: [
|
||||
{ source: 'The New Yorker', time: '40 minutes ago', headline: 'Booker Prize longlist announced, debut novelists dominate' },
|
||||
{ source: 'Pitchfork', time: '2 hours ago', headline: 'Acclaimed composer releases first album in seven years' },
|
||||
{ source: 'Artforum', time: '6 hours ago', headline: 'Venice Biennale attendance surpasses pre-pandemic figures' },
|
||||
],
|
||||
},
|
||||
];
|
||||
defineProps<{
|
||||
categories: Category[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="feed">
|
||||
<section v-for="category in categories" :key="category.label" class="category">
|
||||
<div class="category-header">
|
||||
<span class="category-label">{{ category.label }}</span>
|
||||
<a href="#" class="category-more">More</a>
|
||||
</div>
|
||||
|
||||
<!-- Hero layout for flagged categories -->
|
||||
<template v-if="category.hero">
|
||||
<div class="hero-grid">
|
||||
|
||||
<a v-for="item in category.items.slice(0, 2)"
|
||||
:key="item.headline"
|
||||
href="#"
|
||||
class="hero-story"
|
||||
>
|
||||
<div class="story-img story-img--hero" />
|
||||
<div class="story-body">
|
||||
<div class="story-source">{{ item.source }}</div>
|
||||
<div class="story-headline story-headline--hero">{{ item.headline }}</div>
|
||||
<div v-if="item.deck" class="story-deck">{{ item.deck }}</div>
|
||||
<div class="story-time">{{ item.time }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="story-grid">
|
||||
<a v-for="item in category.items.slice(2)"
|
||||
:key="item.headline"
|
||||
href="#"
|
||||
class="story"
|
||||
>
|
||||
<div class="story-img" />
|
||||
<div class="story-body">
|
||||
<div class="story-source">{{ item.source }}</div>
|
||||
<div class="story-headline">{{ item.headline }}</div>
|
||||
<div class="story-time">{{ item.time }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Standard 3-column grid -->
|
||||
<div v-else class="story-grid">
|
||||
|
||||
<a v-for="item in category.items"
|
||||
:key="item.headline"
|
||||
href="#"
|
||||
class="story"
|
||||
>
|
||||
<div class="story-img" />
|
||||
<div class="story-body">
|
||||
<div class="story-source">{{ item.source }}</div>
|
||||
<div class="story-headline">{{ item.headline }}</div>
|
||||
<div class="story-time">{{ item.time }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<CategorySection
|
||||
v-for="(category, index) in categories"
|
||||
:key="category.label"
|
||||
:category="category"
|
||||
:index="index"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -171,112 +41,4 @@ const categories: Category[] = [
|
||||
.feed
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.category
|
||||
border-bottom: 1px solid #E8E8E8
|
||||
padding: 1.5rem 0
|
||||
|
||||
&:last-child
|
||||
border-bottom: none
|
||||
|
||||
.category-header
|
||||
display: flex
|
||||
align-items: baseline
|
||||
justify-content: space-between
|
||||
margin-bottom: 1rem
|
||||
|
||||
.category-label
|
||||
font-size: 10px
|
||||
font-weight: 600
|
||||
letter-spacing: 2px
|
||||
text-transform: uppercase
|
||||
color: #111
|
||||
|
||||
.category-more
|
||||
font-size: 12px
|
||||
color: #C0392B
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
// Hero grid (2 columns)
|
||||
.hero-grid
|
||||
display: grid
|
||||
grid-template-columns: 1fr 1fr
|
||||
gap: 1em
|
||||
background: #E8E8E8
|
||||
margin-bottom: 1px
|
||||
|
||||
.hero-story
|
||||
background: #F7F7F5
|
||||
text-decoration: none
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&:hover
|
||||
background: #fff
|
||||
|
||||
// Standard story grid (3 columns)
|
||||
.story-grid
|
||||
display: grid
|
||||
grid-template-columns: repeat(3, 1fr)
|
||||
gap: 1px
|
||||
background: #E8E8E8
|
||||
|
||||
.story
|
||||
background: #F7F7F5
|
||||
text-decoration: none
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&:hover
|
||||
background: #fff
|
||||
|
||||
// Image placeholders
|
||||
.story-img
|
||||
width: 100%
|
||||
height: 110px
|
||||
background: #DDDDD8
|
||||
flex-shrink: 0
|
||||
|
||||
&--hero
|
||||
height: 200px
|
||||
|
||||
// Story body
|
||||
.story-body
|
||||
padding: 0.85rem
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 5px
|
||||
flex: 1
|
||||
|
||||
.story-source
|
||||
font-size: 10px
|
||||
font-weight: 600
|
||||
letter-spacing: 1px
|
||||
text-transform: uppercase
|
||||
color: #C0392B
|
||||
|
||||
.story-headline
|
||||
font-family: Georgia, serif
|
||||
font-size: 13px
|
||||
font-weight: 700
|
||||
color: #111
|
||||
line-height: 1.35
|
||||
|
||||
&--hero
|
||||
font-size: 18px
|
||||
line-height: 1.25
|
||||
|
||||
.story-deck
|
||||
font-size: 12px
|
||||
color: #555
|
||||
line-height: 1.5
|
||||
|
||||
.story-time
|
||||
font-size: 10px
|
||||
color: #999
|
||||
margin-top: auto
|
||||
padding-top: 4px
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, router } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import { computed, ref } from 'vue';
|
||||
import MainLayout from '@/layouts/MainLayout.vue';
|
||||
import SectionHeading from '@/components/SectionHeading.vue';
|
||||
import CategoryDialog from '@/components/Settings/Categories/CategoryDialog.vue';
|
||||
import type { Category, Feed } from '@/types/types';
|
||||
|
||||
defineOptions({ layout: MainLayout });
|
||||
|
||||
const props = defineProps<{
|
||||
categories: Category[];
|
||||
}>();
|
||||
|
||||
const dialogOpen = ref(false);
|
||||
const editingCategory = ref<Category | null>(null);
|
||||
const dialogDefaultParentId = ref<number | null>(null);
|
||||
|
||||
const parentOptions = computed(() =>
|
||||
props.categories.map((c) => ({ title: c.title, value: c.id })),
|
||||
);
|
||||
|
||||
const openCreate = (parentId: number | null = null) => {
|
||||
editingCategory.value = null;
|
||||
dialogDefaultParentId.value = parentId;
|
||||
dialogOpen.value = true;
|
||||
};
|
||||
|
||||
const openEdit = (category: Category) => {
|
||||
editingCategory.value = category;
|
||||
dialogOpen.value = true;
|
||||
};
|
||||
|
||||
const destroy = (category: Category) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Delete "${category.title}"? This also deletes its subcategories and feeds.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
router.delete(route('categories.destroy', category.id), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
};
|
||||
|
||||
const feedLabel = (feed: Feed) => feed.type.toUpperCase();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Feeds" />
|
||||
|
||||
<VContainer class="py-8" style="max-width: 720px">
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<SectionHeading
|
||||
eyebrow="Settings"
|
||||
title="Feed categories"
|
||||
subtitle="Organize the RSS feeds you follow into categories and subcategories."
|
||||
/>
|
||||
<VBtn color="primary" @click="openCreate()">New category</VBtn>
|
||||
</div>
|
||||
|
||||
<p v-if="!categories.length" class="text-body-2 text-medium-emphasis">
|
||||
No categories yet. Create one to start adding feeds.
|
||||
</p>
|
||||
|
||||
<VCard
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
>
|
||||
<VCardText class="pa-4">
|
||||
<div class="d-flex align-start justify-space-between">
|
||||
<div>
|
||||
<h3 class="text-h6 font-weight-bold mb-1">
|
||||
{{ category.title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="category.description"
|
||||
class="text-body-2 text-medium-emphasis mb-2"
|
||||
>
|
||||
{{ category.description }}
|
||||
</p>
|
||||
<div
|
||||
v-for="feed in category.feeds"
|
||||
:key="feed.id"
|
||||
class="d-flex align-center ga-2 mb-1"
|
||||
>
|
||||
<span class="text-caption text-medium-emphasis">{{
|
||||
feed.url
|
||||
}}</span>
|
||||
<VChip size="x-small" variant="outlined">{{
|
||||
feedLabel(feed)
|
||||
}}</VChip>
|
||||
<VChip
|
||||
v-if="feed.paywall"
|
||||
size="x-small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
>
|
||||
Paywalled
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ga-1">
|
||||
<VBtn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="openEdit(category)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="destroy(category)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider v-if="category.children?.length" class="my-4" />
|
||||
|
||||
<div
|
||||
v-for="child in category.children"
|
||||
:key="child.id"
|
||||
class="mb-3 ml-4"
|
||||
>
|
||||
<div class="d-flex align-start justify-space-between">
|
||||
<div>
|
||||
<h4 class="text-subtitle-1 font-weight-medium mb-1">
|
||||
{{ child.title }}
|
||||
</h4>
|
||||
<p
|
||||
v-if="child.description"
|
||||
class="text-body-2 text-medium-emphasis mb-2"
|
||||
>
|
||||
{{ child.description }}
|
||||
</p>
|
||||
<div
|
||||
v-for="feed in child.feeds"
|
||||
:key="feed.id"
|
||||
class="d-flex align-center ga-2 mb-1"
|
||||
>
|
||||
<span
|
||||
class="text-caption text-medium-emphasis"
|
||||
>{{ feed.url }}</span
|
||||
>
|
||||
<VChip size="x-small" variant="outlined">{{
|
||||
feedLabel(feed)
|
||||
}}</VChip>
|
||||
<VChip
|
||||
v-if="feed.paywall"
|
||||
size="x-small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
>
|
||||
Paywalled
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ga-1">
|
||||
<VBtn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="openEdit(child)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="destroy(child)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="ml-4"
|
||||
@click="openCreate(category.id)"
|
||||
>
|
||||
Add subcategory
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<CategoryDialog
|
||||
v-model="dialogOpen"
|
||||
:category="editingCategory"
|
||||
:parent-options="parentOptions"
|
||||
:default-parent-id="dialogDefaultParentId"
|
||||
/>
|
||||
</VContainer>
|
||||
</template>
|
||||
Reference in New Issue
Block a user