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
_commit: feeb1fb
_src_path: frontend-template
_commit: ad95f52
_src_path: /home/hideyoshi/Programming/Work/HideyoshiSolutions/frontend-template
author_email: vitor@hideyoshi.com.br
author_github_url: https://github.com/HideyoshiNakazone
author_name: Vitor Hideyoshi
@@ -10,4 +10,4 @@ copyright_year: 2026
project_description: Personal Dev Blog
project_name: 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",
"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",
"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": {}
}

View File

@@ -2,6 +2,22 @@
@import 'tw-animate-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 *));
@theme inline {
@@ -128,3 +144,19 @@
@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}`}
suppressHydrationWarning
>
<body className={'h-screen bg-background antialiased'}>
<body className={'relative h-screen bg-background antialiased'}>
<Provider
attribute='class'
defaultTheme='system'
@@ -52,7 +52,7 @@ export default async function RootLayout({
>
<div className='h-full flex flex-col bg-background'>
<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}
</div>
<SiteFooter />

View File

@@ -1,12 +1,12 @@
import lightSiteIcon from '~/public/img/logo/red/icon-512.png';
import darkSiteIcon from '~/public/img/logo/white/icon-512.png';
import lightSiteIcon from '../public/img/logo/red/icon-512.png';
import darkSiteIcon from '../public/img/logo/white/icon-512.png';
export const siteConfig = {
shortName: 'hideyoshi-blog',
name: 'hideyoshi-blog',
slug: 'hideyoshi-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: {
name: 'Vitor Hideyoshi',
email: 'vitor@hideyoshi.com.br',
@@ -35,7 +35,7 @@ export const siteConfig = {
},
copyright: {
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 {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
@@ -14,19 +15,21 @@ import {
InputGroupTextarea,
} from '@/ui/components/shadcn/input-group';
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 slugify from 'slugify';
import { toast } from 'sonner';
import { z } from 'zod';
export const CreateArticleForm = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const formSchema = z.object({
title: z.string().min(3).max(255),
slug: z.string().min(3),
description: z.string().min(10),
coverImageUrl: z.string().url(),
content: z.string().min(10),
coverImageUrl: z.url(),
content: z.instanceof(File),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -36,7 +39,7 @@ export const CreateArticleForm = () => {
slug: '',
description: '',
coverImageUrl: '',
content: '',
content: undefined,
},
});
@@ -50,27 +53,41 @@ export const CreateArticleForm = () => {
form.setValue('slug', slugify(title).toLowerCase());
}, [form, title]);
async function onSubmit(data: z.infer<typeof formSchema>) {
try {
const result = await saveArticle(data);
toast.success('Article created successfully!', {
description: `Article "${result.title}" has been created.`,
position: 'bottom-right',
});
form.reset();
} catch (error) {
toast.error('Failed to create article', {
description:
error instanceof Error
? error.message
: 'An error occurred',
position: 'bottom-right',
});
}
}
const handleFormSubmit = useCallback(
async (data: z.infer<typeof formSchema>) => {
try {
const result = await saveArticle({
...data,
content: await data.content.text(),
});
toast.success('Article created successfully!', {
description: `Article "${result.title}" has been created.`,
position: 'bottom-right',
});
form.reset();
if (fileInputRef.current) {
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 (
<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>
<Controller
name='title'
@@ -166,18 +183,24 @@ export const CreateArticleForm = () => {
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-content'>
Content
Content (Markdown File)
</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field}
id='form-create-article-content'
placeholder='Write your article content here...'
rows={12}
className='min-h-48 resize-none font-mono text-sm'
aria-invalid={fieldState.invalid}
/>
</InputGroup>
<Input
ref={fileInputRef}
id='form-create-article-content'
type='file'
accept='.md,.markdown'
aria-invalid={fieldState.invalid}
onChange={(event) =>
field.onChange(
event.target.files &&
event.target.files[0]
)
}
/>
<FieldDescription>
Select your article.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}

View File

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