WIP
Some checks failed
Build and Test / run-test (20.x) (push) Failing after 1m36s

This commit is contained in:
2026-04-09 20:57:35 -03:00
parent 9c0006e2dc
commit 3f9613d98b
12 changed files with 1631 additions and 52 deletions

View File

@@ -1,6 +1,6 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: feeb1fb _commit: ad95f52
_src_path: frontend-template _src_path: /home/hideyoshi/Programming/Work/HideyoshiSolutions/frontend-template
author_email: vitor@hideyoshi.com.br author_email: vitor@hideyoshi.com.br
author_github_url: https://github.com/HideyoshiNakazone author_github_url: https://github.com/HideyoshiNakazone
author_name: Vitor Hideyoshi author_name: Vitor Hideyoshi
@@ -10,4 +10,4 @@ copyright_year: 2026
project_description: Personal Dev Blog project_description: Personal Dev Blog
project_name: hideyoshi-blog project_name: hideyoshi-blog
project_slug: hideyoshi-blog project_slug: hideyoshi-blog
project_url: https://blog.hideyoshi.com.br project_url: https://hideyoshi.com.br

50
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Build and Test
on:
push:
jobs:
run-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
lfs: 'true'
- name: Cache node modules
id: cache-npm
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-
${{ runner.os }}-node-
${{ runner.os }}-
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- name: Run Lint
run: npm run lint
- name: Run Build
run: npm run build
env:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
- name: Run Tests
run: npm run test

View File

@@ -12,14 +12,14 @@
}, },
"iconLibrary": "lucide", "iconLibrary": "lucide",
"rtl": false, "rtl": false,
"aliases": {
"components": "@/ui/components",
"ui": "@/ui/components/shadcn",
"lib": "@/ui/components/shadcn/lib",
"utils": "@/ui/components/shadcn/lib/utils",
"hooks": "@/ui/hooks"
},
"menuColor": "default", "menuColor": "default",
"menuAccent": "subtle", "menuAccent": "subtle",
"aliases": {
"components": "@/ui/components",
"utils": "@/ui/components/shadcn/lib/utils",
"ui": "@/ui/components/shadcn",
"lib": "@/ui/components/shadcn/lib",
"hooks": "@/ui/hooks"
},
"registries": {} "registries": {}
} }

View File

@@ -2,6 +2,22 @@
@import 'tw-animate-css'; @import 'tw-animate-css';
@import 'shadcn/tailwind.css'; @import 'shadcn/tailwind.css';
html {
font-family: var(--font-source-code-pro), sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-bold;
font-family: var(--font-montserrat), sans-serif;
}
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
@@ -128,3 +144,19 @@
@apply font-sans; @apply font-sans;
} }
} }
@layer components {
.header-height {
@apply h-[8dvh] md:min-h-[60px];
}
.footer-height {
@apply md:h-24;
}
.page-height {
@apply min-h-[calc(100dvh-8dvh)];
}
.content-height {
@apply min-h-[calc(100dvh-8dvh-6rem)];
}
}

View File

@@ -43,7 +43,7 @@ export default async function RootLayout({
className={`${montserrat.className} ${sourceCodePro.className}`} className={`${montserrat.className} ${sourceCodePro.className}`}
suppressHydrationWarning suppressHydrationWarning
> >
<body className={'h-screen bg-background antialiased'}> <body className={'relative h-screen bg-background antialiased'}>
<Provider <Provider
attribute='class' attribute='class'
defaultTheme='system' defaultTheme='system'
@@ -52,7 +52,7 @@ export default async function RootLayout({
> >
<div className='h-full flex flex-col bg-background'> <div className='h-full flex flex-col bg-background'>
<SiteHeader /> <SiteHeader />
<div className='min-h-[90%] flex flex-col items-center justify-center'> <div className='content-height flex flex-col items-center justify-center'>
{children} {children}
</div> </div>
<SiteFooter /> <SiteFooter />

View File

@@ -1,12 +1,12 @@
import lightSiteIcon from '~/public/img/logo/red/icon-512.png'; import lightSiteIcon from '../public/img/logo/red/icon-512.png';
import darkSiteIcon from '~/public/img/logo/white/icon-512.png'; import darkSiteIcon from '../public/img/logo/white/icon-512.png';
export const siteConfig = { export const siteConfig = {
shortName: 'hideyoshi-blog', shortName: 'hideyoshi-blog',
name: 'hideyoshi-blog', name: 'hideyoshi-blog',
slug: 'hideyoshi-blog', slug: 'hideyoshi-blog',
description: 'Personal Dev Blog', description: 'Personal Dev Blog',
url: process.env.FRONTEND_PATH || 'https://blog.hideyoshi.com.br', url: process.env.FRONTEND_PATH || 'https://hideyoshi.com.br',
author: { author: {
name: 'Vitor Hideyoshi', name: 'Vitor Hideyoshi',
email: 'vitor@hideyoshi.com.br', email: 'vitor@hideyoshi.com.br',
@@ -35,7 +35,7 @@ export const siteConfig = {
}, },
copyright: { copyright: {
company: '', company: '',
initialYear: '2026', initialYear: 2026,
}, },
}; };

View File

@@ -4,6 +4,7 @@ import { saveArticle } from '@/lib/feature/article/article.external';
import { Button } from '@/ui/components/shadcn/button'; import { Button } from '@/ui/components/shadcn/button';
import { import {
Field, Field,
FieldDescription,
FieldError, FieldError,
FieldGroup, FieldGroup,
FieldLabel, FieldLabel,
@@ -14,19 +15,21 @@ import {
InputGroupTextarea, InputGroupTextarea,
} from '@/ui/components/shadcn/input-group'; } from '@/ui/components/shadcn/input-group';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form'; import { Controller, useForm, useWatch } from 'react-hook-form';
import slugify from 'slugify'; import slugify from 'slugify';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
export const CreateArticleForm = () => { export const CreateArticleForm = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const formSchema = z.object({ const formSchema = z.object({
title: z.string().min(3).max(255), title: z.string().min(3).max(255),
slug: z.string().min(3), slug: z.string().min(3),
description: z.string().min(10), description: z.string().min(10),
coverImageUrl: z.string().url(), coverImageUrl: z.url(),
content: z.string().min(10), content: z.instanceof(File),
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@@ -36,7 +39,7 @@ export const CreateArticleForm = () => {
slug: '', slug: '',
description: '', description: '',
coverImageUrl: '', coverImageUrl: '',
content: '', content: undefined,
}, },
}); });
@@ -50,27 +53,41 @@ export const CreateArticleForm = () => {
form.setValue('slug', slugify(title).toLowerCase()); form.setValue('slug', slugify(title).toLowerCase());
}, [form, title]); }, [form, title]);
async function onSubmit(data: z.infer<typeof formSchema>) { const handleFormSubmit = useCallback(
try { async (data: z.infer<typeof formSchema>) => {
const result = await saveArticle(data); try {
toast.success('Article created successfully!', { const result = await saveArticle({
description: `Article "${result.title}" has been created.`, ...data,
position: 'bottom-right', content: await data.content.text(),
}); });
form.reset(); toast.success('Article created successfully!', {
} catch (error) { description: `Article "${result.title}" has been created.`,
toast.error('Failed to create article', { position: 'bottom-right',
description: });
error instanceof Error form.reset();
? error.message
: 'An error occurred', if (fileInputRef.current) {
position: 'bottom-right', fileInputRef.current.value = '';
}); }
} } catch (error) {
} toast.error('Failed to create article', {
description:
error instanceof Error
? error.message
: 'An error occurred',
position: 'bottom-right',
});
}
},
[form]
);
return ( return (
<form id='form-create-article' onSubmit={form.handleSubmit(onSubmit)}> <form
id='form-create-article'
// eslint-disable-next-line react-hooks/refs
onSubmit={form.handleSubmit(handleFormSubmit)}
>
<FieldGroup> <FieldGroup>
<Controller <Controller
name='title' name='title'
@@ -166,18 +183,24 @@ export const CreateArticleForm = () => {
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}> <Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-content'> <FieldLabel htmlFor='form-create-article-content'>
Content Content (Markdown File)
</FieldLabel> </FieldLabel>
<InputGroup> <Input
<InputGroupTextarea ref={fileInputRef}
{...field} id='form-create-article-content'
id='form-create-article-content' type='file'
placeholder='Write your article content here...' accept='.md,.markdown'
rows={12} aria-invalid={fieldState.invalid}
className='min-h-48 resize-none font-mono text-sm' onChange={(event) =>
aria-invalid={fieldState.invalid} field.onChange(
/> event.target.files &&
</InputGroup> event.target.files[0]
)
}
/>
<FieldDescription>
Select your article.
</FieldDescription>
{fieldState.invalid && ( {fieldState.invalid && (
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />
)} )}

View File

@@ -3,8 +3,8 @@ import { BlankUserButton } from '@/ui/components/internal/user-profile/user-prof
export const StaticDesktopHeader = () => { export const StaticDesktopHeader = () => {
const links = [ const links = [
{ href: '/home', label: 'Home' }, { href: '/home', label: 'Home', condition: true },
{ href: '/about', label: 'About' }, { href: '/about', label: 'About', condition: true },
]; ];
return <BaseDesktopHeader links={links} userButton={<BlankUserButton />} />; return <BaseDesktopHeader links={links} userButton={<BlankUserButton />} />;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
import { useIsomorphicLayoutEffect } from '@/ui/hooks/use-isomorphic-layout-effect';
import * as React from 'react';
function useAsRef<T>(props: T) {
const ref = React.useRef<T>(props);
useIsomorphicLayoutEffect(() => {
ref.current = props;
});
return ref;
}
export { useAsRef };

View File

@@ -0,0 +1,6 @@
import * as React from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
export { useIsomorphicLayoutEffect };

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
function useLazyRef<T>(fn: () => T) {
const ref = React.useRef<T | null>(null);
if (ref.current === null) {
ref.current = fn();
}
return ref as React.RefObject<T>;
}
export { useLazyRef };