[socialmon] ui: tweaks

This commit is contained in:
Yangshun 2025-07-02 10:48:46 +08:00
parent 63091c0951
commit f503a4cdcd
20 changed files with 95 additions and 78 deletions

View File

@ -1,11 +1,10 @@
import type { Metadata } from 'next';
import { redirectToLoginPageIfNotLoggedIn } from '~/components/auth/redirectToLoginPageIfNotLoggedIn';
import ProjectsPage from '~/components/project/ProjectsPage';
import { getUser } from '~/app/lib/auth';
import ProjectsPage from './ProjectsPage';
export const metadata: Metadata = {
description: 'Social moderator',
title: 'SocialMon | Projects',

View File

@ -1,9 +1,9 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getUser } from '~/app/lib/auth';
import ProjectCreatePage from '~/components/project/ProjectCreatePage';
import ProjectCreatePage from './ProjectCreatePage';
import { getUser } from '~/app/lib/auth';
export const metadata: Metadata = {
description: 'Social moderator',

View File

@ -17,7 +17,8 @@ export default function RootLayout({ children }: Props) {
<html lang="en">
<body className={clsx('antialiased')}>
<GlobalProviders>
<MantineProvider>
<MantineProvider
theme={{ defaultRadius: 'md', primaryColor: 'orange' }}>
<CustomToaster />
<div className="bg-white">{children}</div>
</MantineProvider>

View File

@ -34,7 +34,7 @@ export default async function Layout({ children, params }: Props) {
return (
<div className="flex min-h-screen flex-col">
<ProjectsNavbar user={user} />
<Container className={clsx('flex-1', 'p-4', 'flex')}>
<Container className={clsx('flex-1', 'px-4', 'flex')}>
{children}
</Container>
</div>

View File

@ -1,8 +1,8 @@
import { notFound } from 'next/navigation';
import prisma from '~/server/prisma';
import PostDetailPage from '~/components/posts/PostDetailPage';
import PostDetailPage from './PostDetailPage';
import prisma from '~/server/prisma';
type Props = Readonly<{
params: {

View File

@ -49,7 +49,11 @@ export default function ActivityLogList() {
{hasNextPage && (
<div className="flex w-full justify-center py-6">
<Button disabled={isFetchingNextPage} onClick={() => fetchNextPage()}>
<Button
disabled={isFetchingNextPage}
size="sm"
variant="default"
onClick={() => fetchNextPage()}>
{isFetchingNextPage ? 'Loading more...' : 'See more'}
</Button>
</div>

View File

@ -71,14 +71,14 @@ export default function PostDetailPage({ post }: Props) {
}
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div>
<Button
component={Link}
href={`/projects/${projectSlug}`}
leftSection={<RiArrowLeftLine />}
size="xs"
variant="outline">
variant="light">
Back
</Button>
</div>

View File

@ -97,7 +97,7 @@ export default function PostDetail({
return (
<div>
<Flex direction="column" gap={2} justify="space-between" mb="xs" mt="md">
<Title order={3}>{post.title}</Title>
<Title order={2}>{post.title}</Title>
<PostMetadata post={post} showViewPost={true} />
</Flex>
<Text size="sm">
@ -108,7 +108,6 @@ export default function PostDetail({
className="prose"
/>
</Text>
{!post.reply && (
<>
<Divider my="md" />

View File

@ -1,4 +1,4 @@
import { Box, Title } from '@mantine/core';
import { Anchor, Box, Title } from '@mantine/core';
import clsx from 'clsx';
import Link from 'next/link';
@ -34,17 +34,21 @@ export default function PostItem({
<Box
className={clsx(
'w-full',
'p-2',
'px-1',
'py-4',
'rounded',
'hover:bg-slate-100',
'transition-all duration-200',
'cursor-pointer',
'flex flex-col gap-2',
'text-left',
)}
component={Link}
href={`/projects/${projectSlug}/posts/${post.id}`}>
<Title order={5}>{post.title}</Title>
)}>
<Anchor
component={Link}
href={`/projects/${projectSlug}/posts/${post.id}`}
underline="hover">
<Title order={3} size="h4">
{post.title}
</Title>
</Anchor>
<PostMetadata
post={post as PostExtended}
showMarkedAsIrrelevant={showMarkedAsIrrelevant}

View File

@ -1,6 +1,6 @@
'use client';
import { Button, Tabs, Text, Tooltip } from '@mantine/core';
import { Box, Button, Tabs, Text, Tooltip } from '@mantine/core';
import clsx from 'clsx';
import { useState } from 'react';
@ -52,21 +52,30 @@ export default function PostList() {
const posts = data?.pages.flatMap((page) => page.posts);
return (
<div className={clsx('flex flex-col gap-2', 'h-full w-full')}>
<div className={clsx('flex flex-col', 'w-full')}>
<div
className="sticky w-full bg-white"
className="sticky w-full bg-white pt-4"
style={{ top: `${NAVBAR_HEIGHT}px` }}>
<Tabs
value={activeTab}
variant="outline"
onChange={(value) => setActiveTab(value as PostTab)}>
<Tabs.List>
<Tabs.Tab value="unreplied">Unreplied</Tabs.Tab>
<Tabs.Tab value="replied">Replied</Tabs.Tab>
<Tabs.Tab value="irrelevant">Irrelevant</Tabs.Tab>
<Tabs.Tab value="all">All</Tabs.Tab>
<Tabs.Tab fw={500} value="unreplied">
Unreplied
</Tabs.Tab>
<Tabs.Tab fw={500} value="replied">
Replied
</Tabs.Tab>
<Tabs.Tab fw={500} value="irrelevant">
Irrelevant
</Tabs.Tab>
<Tabs.Tab fw={500} value="all">
All
</Tabs.Tab>
</Tabs.List>
</Tabs>
<div className="absolute right-1 top-0.5 flex items-center gap-2 md:right-4">
<div className="absolute right-0 top-3 flex items-center gap-2">
{projectData?.postsLastFetchedAt && (
<div className="hidden md:block">
<Tooltip
@ -74,7 +83,7 @@ export default function PostList() {
new Date(projectData.postsLastFetchedAt),
)}
withArrow={true}>
<Text size="sm">
<Text c="dimmed" size="sm">
Fetched{' '}
<RelativeTimestamp
timestamp={new Date(projectData.postsLastFetchedAt)}
@ -86,16 +95,13 @@ export default function PostList() {
<FetchPostButton />
</div>
</div>
<Text hidden={!isLoading} size="md">
Loading...
</Text>
{!isLoading && posts?.length === 0 && (
<Text size="md">No post found</Text>
)}
<div className={clsx('divide-y')}>
<Box py={4}>
{posts?.map((post) => (
<PostItem
key={post.id}
@ -104,17 +110,17 @@ export default function PostList() {
showRepliedBadge={activeTab === 'all'}
/>
))}
{hasNextPage && (
<div className="flex w-full justify-center py-6">
<Button
disabled={isFetchingNextPage}
variant="default"
onClick={() => fetchNextPage()}>
{isFetchingNextPage ? 'Loading more...' : 'See more'}
</Button>
</div>
)}
</div>
</Box>
</div>
);
}

View File

@ -1,9 +1,7 @@
import { Badge, Button, Pill, Text, Tooltip } from '@mantine/core';
import { Anchor, Badge, Button, Pill, Text, Tooltip } from '@mantine/core';
import Link from 'next/link';
import { RiArrowRightUpLine, RiCheckLine } from 'react-icons/ri';
import RelativeTimestamp from '~/components/common/datetime/RelativeTimestamp';
import type { PostExtended } from '~/types';
import { redditPermalinkToUrl } from '../utils';
@ -16,7 +14,7 @@ type Props = Readonly<{
showViewPost?: boolean;
}>;
function PostMetadata({
export default function PostMetadata({
post,
showMarkedAsIrrelevant,
showRepliedBadge,
@ -27,32 +25,43 @@ function PostMetadata({
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<PostStats post={post} />
<div className="h-1 w-1 rounded-full bg-slate-600" />
<Tooltip label="Post fetched at" withArrow={true}>
<Text size="sm">
<RelativeTimestamp timestamp={new Date(post.createdAt)} />
<Text c="dimmed" size="sm">
{new Intl.DateTimeFormat(undefined, {
day: 'numeric',
hour: 'numeric',
hour12: true,
minute: '2-digit',
month: 'long',
weekday: 'long',
year: 'numeric',
}).format(post.createdAt)}
</Text>
</Tooltip>
<div className="h-1 w-1 rounded-full bg-slate-600" />
<Text size="sm">{post.subreddit}</Text>
<Text size="sm">
<Anchor
className="z-1"
href={`https://reddit.com/${post.subreddit}`}
target="_blank"
underline="hover">
{post.subreddit}
</Anchor>
</Text>
</div>
{showViewPost && (
<Button
color="orange"
component={Link}
href={redditPermalinkToUrl(post.permalink)}
rightSection={<RiArrowRightUpLine />}
target="_blank"
variant="subtle">
View Post
View on Reddit
</Button>
)}
</div>
{post.keywords.length > 0 && (
<div className="flex flex-wrap gap-1">
{post.keywords.map((keyword) => (
@ -62,7 +71,6 @@ function PostMetadata({
))}
</div>
)}
{post.reply && showRepliedBadge && (
<div className="flex items-center gap-2">
<Badge color="violet" leftSection={<RiCheckLine />} size="xs">
@ -73,12 +81,10 @@ function PostMetadata({
{post.relevancy === 'IRRELEVANT' && showMarkedAsIrrelevant && (
<div className="flex items-center gap-2">
<Badge color="violet" leftSection={<RiCheckLine />} size="xs">
Marked as Irrelevant
Marked as irrelevant
</Badge>
</div>
)}
</div>
);
}
export default PostMetadata;

View File

@ -32,12 +32,11 @@ export default function PostResponse({
rightSection={<RiArrowRightUpLine />}
target="_blank"
variant="subtle">
View Reply
View reply
</Button>
</div>
</div>
{/* Fallback to showing the stored relied if fetching reply failed */}
{/* Fallback to showing the stored reply if fetching reply failed */}
{!isFetchingComments && !comments?.data?.children?.length ? (
<PostCommentsList
comments={{

View File

@ -49,6 +49,7 @@ export default function PostRelevancyActionButton({
disabled={markPostRelevancyMutation.isLoading}
loading={markPostRelevancyMutation.isLoading}
size="xs"
variant="light"
onClick={onMarkPostRelevancy}>
{relevancy === PostRelevancy.IRRELEVANT
? 'Mark as relevant'

View File

@ -83,7 +83,6 @@ export default function PostComment({ className, comment, level }: Props) {
<RelativeTimestamp timestamp={new Date(created_utc * 1000)} />
</Text>
</div>
<Text size="sm">
<span
dangerouslySetInnerHTML={{ __html: content }}

View File

@ -111,7 +111,7 @@ export default function ProjectForm({
required={true}
{...form.getInputProps('name')}
/>
<div>
<label className="mb-1 block font-semibold">
Keyword/Subreddit Groups

View File

@ -105,7 +105,7 @@ export default function ProjectsProductsToAdvertiseInput() {
{form.errors.productsToAdvertise}
</Text>
)}
<Button onClick={addProduct}>Add Product</Button>
<Button onClick={addProduct}>Add product</Button>
</Fieldset>
);
}

View File

@ -56,31 +56,29 @@ export default function ProjectsPage({ isAdminRole }: Props) {
data?.map((project) => (
<Card
key={project.id}
className="flex flex-col gap-6"
padding="sm"
shadow="sm"
className="flex flex-col gap-2"
padding="lg"
radius="lg"
withBorder={true}>
<Text fw={500} size="lg">
{project.name}
</Text>
<div className="flex flex-col gap-2">
<Text>
{project.productsToAdvertise?.length || 0} products to
advertise
<Text size="sm">
{project.productsToAdvertise?.length || 0} product(s)
</Text>
{/* Aggregate keyword and subreddit counts from subredditKeywords */}
{project.subredditKeywords &&
project.subredditKeywords.length > 0 ? (
<>
<Text>
<Text size="sm">
{project.subredditKeywords.reduce(
(acc, group) => acc + (group.keywords?.length || 0),
0,
)}{' '}
keywords (grouped)
</Text>
<Text>
<Text size="sm">
{
Array.from(
new Set(
@ -95,8 +93,12 @@ export default function ProjectsPage({ isAdminRole }: Props) {
</>
) : (
<>
<Text>{project.keywords.length} keywords (legacy)</Text>
<Text>{project.subreddits.length} subreddits (legacy)</Text>
<Text size="sm">
{project.keywords.length} keywords (legacy)
</Text>
<Text size="sm">
{project.subreddits.length} subreddits (legacy)
</Text>
</>
)}
</div>
@ -104,7 +106,6 @@ export default function ProjectsPage({ isAdminRole }: Props) {
className="absolute inset-0"
href={`/projects/${project.slug}`}
/>
<Menu position="bottom-end" shadow="sm">
<Menu.Target>
<div
@ -115,7 +116,6 @@ export default function ProjectsPage({ isAdminRole }: Props) {
</ActionIcon>
</div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<RiEyeLine />}

View File

@ -1,3 +1,4 @@
import { Text } from '@mantine/core';
import clsx from 'clsx';
import Link from 'next/link';
import type { ReactNode } from 'react';
@ -25,13 +26,12 @@ export default function Navbar({ navItems, navUser }: Props) {
className={clsx('px-4', 'flex items-center justify-between gap-2')}>
<div className="flex items-center gap-6">
<Link href="/">
<span className="text-xl font-bold tracking-tighter md:text-3xl">
SocialMon
</span>
<Text fw={600} lts="1px" size="md" tt="uppercase">
Socialmon
</Text>
</Link>
{navItems}
</div>
{navUser}
</Container>
</header>

View File

@ -37,7 +37,6 @@ export default function UserCard({ user }: Props) {
className="relative flex flex-col gap-2"
mb="md"
padding="lg"
radius="md"
shadow="sm"
withBorder={true}>
<Menu position="bottom-end" shadow="sm" withinPortal={true}>