refactor: improve news and email handling, style, folder structure (#16)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cn } from '@utils/ui';
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../utils/ui';
|
||||
|
||||
export type ButtonProps = {
|
||||
asChild?: boolean;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@utils/ui';
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../utils/ui';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -7,10 +7,7 @@ const Card = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||
className
|
||||
)}
|
||||
className={cn('rounded-lg bg-card text-card-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -32,12 +29,9 @@ const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
<h1
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-3xl font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
className={cn('leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -1,32 +1,30 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Card as CardUI
|
||||
} from '../../components/ui/card';
|
||||
import Footer from './footer';
|
||||
CardTitle
|
||||
} from './Card';
|
||||
import Footer from './Footer';
|
||||
|
||||
type CardProps = {
|
||||
interface CardProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
content: ReactNode;
|
||||
style?: string;
|
||||
className?: string;
|
||||
footer?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
export default function CustomCard({
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
style,
|
||||
className,
|
||||
footer = true
|
||||
}: CardProps) => {
|
||||
}: CardProps) {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const isMobile = useMediaQuery({ query: '(max-width: 767px)' });
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoaded(true);
|
||||
@@ -37,13 +35,8 @@ export const Card = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='gradient-border'>
|
||||
<CardUI
|
||||
style={{
|
||||
boxShadow: '0 16px 32px 0 rgba(0, 0, 0, 0.6)'
|
||||
}}
|
||||
className={`max-h-[90vh] max-w-[90vw] p-8 ${style}`}
|
||||
>
|
||||
<div className='gradient-border shadow-2xl shadow-black'>
|
||||
<Card className={`z-10 max-w-[90vw] p-8 ${className}`}>
|
||||
<CardHeader>
|
||||
<p className='text-xs uppercase text-gray-500'>
|
||||
Hackernews + newsletter
|
||||
@@ -51,19 +44,13 @@ export const Card = ({
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
{isMobile ? (
|
||||
<CardContent>{content}</CardContent>
|
||||
) : (
|
||||
<CardContent className='flex max-h-[60vh] flex-grow justify-center overflow-auto'>
|
||||
{content}
|
||||
</CardContent>
|
||||
)}
|
||||
<CardContent>{content}</CardContent>
|
||||
{footer && (
|
||||
<CardFooter>
|
||||
<Footer />
|
||||
</CardFooter>
|
||||
)}
|
||||
</CardUI>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '../ui/button';
|
||||
import { Button } from './Button';
|
||||
|
||||
type LinkProps = {
|
||||
interface LinkProps {
|
||||
path: string;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function CustomLink({ path, text }: LinkProps) {
|
||||
export default function CustomLink({ path, text }: LinkProps) {
|
||||
return (
|
||||
<Button asChild>
|
||||
<Link href={path}>{text}</Link>
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { CustomLink } from './customLink';
|
||||
import CustomLink from './CustomLink';
|
||||
|
||||
const links = [{ name: 'Subscribe', path: '/' }];
|
||||
|
||||
function Footer() {
|
||||
export default function Footer() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
@@ -22,7 +22,7 @@ function Footer() {
|
||||
.
|
||||
</p>
|
||||
) : (
|
||||
<ul className='flex justify-center space-x-4'>
|
||||
<div className='flex justify-center space-x-4'>
|
||||
{links.map(
|
||||
link =>
|
||||
pathname !== link.path &&
|
||||
@@ -33,10 +33,8 @@ function Footer() {
|
||||
{pathname === '/privacy' && (
|
||||
<CustomLink path='/unsubscribe' text='Unsubscribe' />
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@utils/ui';
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../utils/ui';
|
||||
|
||||
const Input = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
@@ -9,7 +9,7 @@ const Input = React.forwardRef<
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cn } from '@utils/ui';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../utils/ui';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { NewsTileSchema } from '../../../../utils/schemas';
|
||||
import { TileContent } from './tileContent';
|
||||
|
||||
type CardProps = {
|
||||
newsA?: z.infer<typeof NewsTileSchema>;
|
||||
newsB?: z.infer<typeof NewsTileSchema>;
|
||||
};
|
||||
|
||||
export function Tile({ newsA, newsB }: CardProps) {
|
||||
const [switched, setSwitched] = useState(false);
|
||||
const [active, setActive] = useState(Math.random() < 0.5);
|
||||
const [delayed, setDelayed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const randomDelay = Math.floor(Math.random() * 10000);
|
||||
|
||||
const interval = setInterval(
|
||||
() => {
|
||||
setSwitched(true);
|
||||
|
||||
window.setTimeout(function () {
|
||||
setSwitched(false);
|
||||
setActive(!active);
|
||||
setDelayed(false);
|
||||
}, 500 / 2);
|
||||
},
|
||||
delayed ? randomDelay : randomDelay + 10000
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [active, delayed]);
|
||||
|
||||
if (!newsA || !newsB) return <div></div>;
|
||||
|
||||
return (
|
||||
<div className={`transform ${switched ? 'animate-rotate' : ''}`}>
|
||||
<div className='transform-gpu'>
|
||||
<div className={`absolute left-0 top-0 w-full ${''}`}>
|
||||
{active
|
||||
? TileContent({ story: newsA, side: true })
|
||||
: TileContent({ story: newsB, side: false })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { getRandomGrey } from '../../../../utils/getRandomGrey';
|
||||
import { NewsTileSchema } from '../../../../utils/schemas';
|
||||
|
||||
type CardContentProps = {
|
||||
story: z.infer<typeof NewsTileSchema>;
|
||||
side: boolean;
|
||||
};
|
||||
|
||||
export function TileContent({ story, side }: CardContentProps) {
|
||||
const [firstColor, setFirstColor] = useState(getRandomGrey());
|
||||
const [secondColor, setSecondColor] = useState(getRandomGrey());
|
||||
const [switched, setSwitched] = useState(true);
|
||||
|
||||
if (switched !== side) {
|
||||
setFirstColor(getRandomGrey());
|
||||
setSecondColor(getRandomGrey());
|
||||
|
||||
setSwitched(side);
|
||||
}
|
||||
|
||||
const color = side ? firstColor : secondColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-40 w-40 overflow-hidden rounded-lg p-6 shadow-sm`}
|
||||
style={{
|
||||
backgroundColor: `${color}`,
|
||||
color: '#808080'
|
||||
}}
|
||||
>
|
||||
<h1 className='overflow-auto font-semibold'>{story.title}</h1>
|
||||
<p className='overflow-auto italic'>by {story.by}</p>
|
||||
<div
|
||||
className='rounded-lg'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: '33.33%',
|
||||
background: `linear-gradient(to bottom, transparent, ${color})`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Note } from './components/note';
|
||||
import Template from './template';
|
||||
import Note from './components/Note';
|
||||
import Template from './Template';
|
||||
|
||||
export default function ConfirmationTemplate(code: string) {
|
||||
return {
|
||||
@@ -1,12 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { NewsSchema } from '../../utils/schemas';
|
||||
import { textTruncate } from '../../utils/textTruncate';
|
||||
import { sayings } from './helpers/sayings';
|
||||
import Template from './template';
|
||||
import { sayings } from '@utils/sayings';
|
||||
import { textTruncate } from '@utils/textTruncate';
|
||||
import { NewsType } from '@utils/validationSchemas';
|
||||
import Template from './Template';
|
||||
|
||||
export default function NewsletterTemplate(
|
||||
stories: z.infer<typeof NewsSchema>[]
|
||||
) {
|
||||
export default function NewsletterTemplate(stories: NewsType[]) {
|
||||
return {
|
||||
subject: `What's new from the Hackernews forum?`,
|
||||
template: (
|
||||
@@ -40,9 +37,7 @@ export default function NewsletterTemplate(
|
||||
paddingRight: '1.5rem'
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: '600' }}>
|
||||
{story.title}
|
||||
</h2>
|
||||
<h3>{story.title}</h3>
|
||||
<p style={{ fontSize: '1rem', fontStyle: 'italic' }}>
|
||||
by {story.by}
|
||||
</p>
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Footer } from './components/footer';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
type TemplateProps = {
|
||||
interface TemplateProps {
|
||||
title: string;
|
||||
body: JSX.Element;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Template({ title, body }: TemplateProps) {
|
||||
return (
|
||||
@@ -12,21 +12,19 @@ export default function Template({ title, body }: TemplateProps) {
|
||||
maxWidth: '720px',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F9FBFB',
|
||||
backgroundColor: '#F9FBFB'
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
<h2
|
||||
style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
backgroundColor: `#8230CC`,
|
||||
backgroundColor: `#8230CC`
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
</h2>
|
||||
<div style={{ margin: '20px', padding: '20px' }}>{body}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Note } from './components/note';
|
||||
import Template from './template';
|
||||
import Note from './components/Note';
|
||||
import Template from './Template';
|
||||
|
||||
export default function UnsubscribeTemplate() {
|
||||
return {
|
||||
@@ -1,4 +1,4 @@
|
||||
export function Footer() {
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer
|
||||
style={{
|
||||
@@ -1,8 +1,8 @@
|
||||
type NoteProps = {
|
||||
interface NoteProps {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
export function Note({ children }: NoteProps) {
|
||||
export default function Note({ children }: NoteProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -1,13 +0,0 @@
|
||||
export const sayings = [
|
||||
'hot off the press',
|
||||
'straight from the oven',
|
||||
"straight from the horse's mouth",
|
||||
'brand spanking new',
|
||||
'fresh as a daisy',
|
||||
'straight out of the box',
|
||||
'straight off the assembly line',
|
||||
'hot out of the kitchen',
|
||||
'just minted',
|
||||
'freshly brewed',
|
||||
'just off the production line'
|
||||
];
|
||||
26
components/form/FormControl.tsx
Normal file
26
components/form/FormControl.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useFormField } from '@hooks/useFormField';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
|
||||
export const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = 'FormControl';
|
||||
20
components/form/FormDescription.tsx
Normal file
20
components/form/FormDescription.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useFormField } from '@hooks/useFormField';
|
||||
import { cn } from '@utils/ui';
|
||||
import * as React from 'react';
|
||||
|
||||
export const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = 'FormDescription';
|
||||
22
components/form/FormLabel.tsx
Normal file
22
components/form/FormLabel.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useFormField } from '@hooks/useFormField';
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cn } from '@utils/ui';
|
||||
import * as React from 'react';
|
||||
import { Label } from '../Label';
|
||||
|
||||
export const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = 'FormLabel';
|
||||
28
components/form/FormMessage.tsx
Normal file
28
components/form/FormMessage.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useFormField } from '@hooks/useFormField';
|
||||
import { cn } from '@utils/ui';
|
||||
import * as React from 'react';
|
||||
|
||||
export const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!body ? null : (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-sm font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FormMessage.displayName = 'FormMessage';
|
||||
@@ -1,16 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { NewsTileType } from '@utils/validationSchemas';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { NewsTile, NewsTileSchema } from '../../../utils/schemas';
|
||||
import { Tile } from './components/tile';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Tile from './components/Tile';
|
||||
|
||||
type TilesProps = {
|
||||
interface TilesProps {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
export const Tiles = ({ children }: TilesProps) => {
|
||||
export default function Tiles({ children }: TilesProps) {
|
||||
const pathname = usePathname();
|
||||
const [windowSize, setWindowSize] = useState<{
|
||||
width: number;
|
||||
@@ -19,15 +18,15 @@ export const Tiles = ({ children }: TilesProps) => {
|
||||
width: 0,
|
||||
height: 0
|
||||
});
|
||||
const [news, setNews] = useState<z.infer<typeof NewsTileSchema>[]>();
|
||||
const [news, setNews] = useState<NewsTileType[]>();
|
||||
|
||||
useEffect(() => {
|
||||
async function getNews() {
|
||||
const news: NewsTile[] = await fetch('/api/news').then(res => res.json());
|
||||
const news: NewsTileType[] = await fetch('/api/news').then(res =>
|
||||
res.json()
|
||||
);
|
||||
|
||||
if (news) {
|
||||
setNews(news);
|
||||
}
|
||||
setNews(news);
|
||||
}
|
||||
|
||||
if (!news) {
|
||||
@@ -50,34 +49,38 @@ export const Tiles = ({ children }: TilesProps) => {
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setWindowSize, news]);
|
||||
}, [news]);
|
||||
|
||||
if (pathname === '/maintenance') return <div>{children}</div>;
|
||||
const renderTile = useCallback(
|
||||
(key: number) => {
|
||||
if (!news) return <div key={key}></div>;
|
||||
|
||||
function renderTile(key: number) {
|
||||
if (!news) return <div key={key}></div>;
|
||||
const randomA = Math.floor(Math.random() * news?.length);
|
||||
const randomB = Math.floor(
|
||||
Math.random() * news?.filter((_, index) => index !== randomA)?.length
|
||||
);
|
||||
|
||||
const randomA = Math.floor(Math.random() * news?.length);
|
||||
const randomB = Math.floor(
|
||||
Math.random() * news?.filter((_, index) => index !== randomA)?.length
|
||||
);
|
||||
return (
|
||||
<div key={key} className={`m-1 h-40 w-40`}>
|
||||
<Tile newsA={news[randomA]} newsB={news[randomB]} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[news]
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} className={`m-1 h-40 w-40`}>
|
||||
<Tile newsA={news[randomA]} newsB={news[randomB]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const renderRow = useCallback(
|
||||
(columns: number, key: number) => {
|
||||
return (
|
||||
<div key={key} className='flex justify-between'>
|
||||
{Array.from({ length: columns }).map((_, index) => renderTile(index))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[renderTile]
|
||||
);
|
||||
|
||||
function renderRow(columns: number, key: number) {
|
||||
return (
|
||||
<div key={key} className='flex justify-between'>
|
||||
{Array.from({ length: columns }).map((_, index) => renderTile(index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const renderGrid = useCallback(() => {
|
||||
const columns = Math.ceil(windowSize.width / (40 * 4));
|
||||
const rows = Math.ceil(windowSize.height / (40 * 4));
|
||||
|
||||
@@ -93,7 +96,9 @@ export const Tiles = ({ children }: TilesProps) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [children, renderRow, windowSize]);
|
||||
|
||||
if (pathname === '/maintenance') return <div>{children}</div>;
|
||||
|
||||
return <div className='flex h-[100vh] overflow-hidden'>{renderGrid()}</div>;
|
||||
};
|
||||
}
|
||||
68
components/tiles/components/Tile.tsx
Normal file
68
components/tiles/components/Tile.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getRandomGrey } from '@utils/getRandomGrey';
|
||||
import { NewsTileType } from '@utils/validationSchemas';
|
||||
import { useEffect, useState } from 'react';
|
||||
import TileContent from './TileContent';
|
||||
|
||||
interface CardProps {
|
||||
newsA?: NewsTileType;
|
||||
newsB?: NewsTileType;
|
||||
}
|
||||
|
||||
const TEN_SECONDS = 10000;
|
||||
const HALF_SECOND = 500;
|
||||
|
||||
export default function Tile({ newsA, newsB }: CardProps) {
|
||||
const [switched, setSwitched] = useState(false);
|
||||
const [active, setActive] = useState(false);
|
||||
const [delayed, setDelayed] = useState(true);
|
||||
const [colorA, setColorA] = useState(getRandomGrey());
|
||||
const [colorB, setColorB] = useState(getRandomGrey());
|
||||
|
||||
useEffect(() => {
|
||||
const randomDelay = Math.floor(Math.random() * TEN_SECONDS);
|
||||
|
||||
const interval = setInterval(
|
||||
() => {
|
||||
setSwitched(true);
|
||||
|
||||
window.setTimeout(function () {
|
||||
setActive(!active);
|
||||
setColorA(getRandomGrey());
|
||||
setColorB(getRandomGrey());
|
||||
}, HALF_SECOND / 2);
|
||||
|
||||
window.setTimeout(function () {
|
||||
setSwitched(false);
|
||||
setDelayed(false);
|
||||
}, HALF_SECOND);
|
||||
},
|
||||
delayed ? randomDelay : randomDelay + TEN_SECONDS
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [active, delayed]);
|
||||
|
||||
if (!newsA || !newsB) return <div></div>;
|
||||
|
||||
return (
|
||||
<div className={`transform ${switched ? 'animate-rotate' : ''}`}>
|
||||
<div className='transform-gpu'>
|
||||
<div className={`absolute left-0 top-0 w-full ${''}`}>
|
||||
{active
|
||||
? TileContent({
|
||||
story: newsA,
|
||||
side: true,
|
||||
firstColor: colorA,
|
||||
secondColor: colorB
|
||||
})
|
||||
: TileContent({
|
||||
story: newsB,
|
||||
side: false,
|
||||
firstColor: colorB,
|
||||
secondColor: colorA
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
components/tiles/components/TileContent.tsx
Normal file
41
components/tiles/components/TileContent.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NewsTileType } from '@utils/validationSchemas';
|
||||
|
||||
interface CardContentProps {
|
||||
story: NewsTileType;
|
||||
side: boolean;
|
||||
firstColor: string;
|
||||
secondColor: string;
|
||||
}
|
||||
|
||||
export default function TileContent({
|
||||
story,
|
||||
side,
|
||||
firstColor,
|
||||
secondColor
|
||||
}: CardContentProps) {
|
||||
const color = side ? firstColor : secondColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-40 w-40 overflow-hidden rounded-lg p-6 shadow-sm`}
|
||||
style={{
|
||||
backgroundColor: `${color}`,
|
||||
color: '#808080'
|
||||
}}
|
||||
>
|
||||
<h4 className='overflow-auto'>{story.title}</h4>
|
||||
<p className='overflow-auto italic'>by {story.by}</p>
|
||||
<div
|
||||
className='rounded-lg'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: '33.33%',
|
||||
background: `linear-gradient(to bottom, transparent, ${color})`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext
|
||||
} from 'react-hook-form';
|
||||
import { Label } from '../../components/ui/label';
|
||||
import { cn } from '../../utils/ui';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error('useFormField should be used within <FormField>');
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = 'FormItem';
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = 'FormLabel';
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = 'FormControl';
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = 'FormDescription';
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!body ? null : (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-sm font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FormMessage.displayName = 'FormMessage';
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField
|
||||
};
|
||||
Reference in New Issue
Block a user