Commit 4c58c646 authored by devteam's avatar devteam

Merge branch 'dev' into 'staging'

fix: add local upload

See merge request !1
parents 92238f07 d5b25004
Pipeline #19012 failed with stage
in 1 minute and 31 seconds
# syntax=docker.io/docker/dockerfile:1 # Etapa 1: build da aplicação
FROM node:22-alpine AS builder
FROM node:22-alpine AS base # Diretório de trabalho dentro do container
# INSTALAÇÃO DE DEPENDENCIAS
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ # Copia os arquivos de dependência
RUN \ COPY package.json package-lock.json ./
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Instala as dependências
RUN npm ci
# REBUILD CODIGO APENAS QUANDO PRECISA # Copia o restante dos arquivos
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 # Build da aplicação Next.js
RUN npm run build
RUN \ # Etapa 2: imagem final para produção
if [ -f yarn.lock ]; then yarn run build; \ FROM node:22-alpine
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Diretório de trabalho
# IMAGEM PARA PRODUCAO, COPIA TODOS OS ARQUIVOS DO REBUILD E EXECUTA
FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production # Copia os arquivos necessários do builder
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ # Garante que a pasta imgs exista
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static RUN mkdir -p ./public/imgs
#USER nextjs
# Expõe a porta padrão
EXPOSE 3000 EXPOSE 3000
ENV PORT=3000 # Comando para iniciar a aplicação
CMD ["npm", "start"]
ENV HOSTNAME="0.0.0.0" \ No newline at end of file
CMD ["node", "server.js"]
\ No newline at end of file
version: '3.9'
services:
nextjs-app:
build: .
ports:
- "3000:3000"
volumes:
- ./imgs:/app/public/imgs
environment:
- NODE_ENV=production
\ No newline at end of file
// next.config.js /** @type {import('next').NextConfig} */
module.exports = { const nextConfig = {
// ... rest of the configuration. images: {
output: "standalone", remotePatterns: [
}; {
protocol: 'https',
hostname: `${process.env.NEXT_PUBLIC_CLOUDFLARE_URL_PUBLIC_BUCKET}`,
port: '',
pathname: '**',
},
],
},
}
module.exports = nextConfig
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -9,39 +9,50 @@ ...@@ -9,39 +9,50 @@
"lint": "eslint . --fix" "lint": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.10.0",
"@mui/material": "^5.16.4", "@mui/material": "^5.16.4",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"axios": "^1.7.2", "@tanstack/react-query": "^5.59.3",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"date-fns-tz": "^3.2.0",
"embla-carousel-autoplay": "^8.1.8", "embla-carousel-autoplay": "^8.1.8",
"embla-carousel-react": "^8.1.8", "embla-carousel-react": "^8.1.8",
"lucide-react": "^0.383.0", "lucide-react": "^0.383.0",
"next": "14.2.3", "next": "14.2.3",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"nodemailer": "^6.10.0",
"nookies": "^2.5.2",
"path": "^0.12.7",
"react": "^18", "react": "^18",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.52.2", "react-hook-form": "^7.52.2",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@rocketseat/eslint-config": "^2.2.2", "@rocketseat/eslint-config": "^2.2.2",
"@types/cookie": "^0.6.0",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-input-mask": "^3.0.5", "@types/react-input-mask": "^3.0.5",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import { api } from '@/lib/axios'
export type AccountsProps = {
id: string
name: string
email: string
}
export async function getAccounts() {
const accounts = await api.get<AccountsProps[]>('/accounts')
return accounts
}
export async function getAccountsId(id: string) {
const account = await api.get<AccountsProps>(`/accounts/${id}`)
return account
}
import { api } from '@/lib/axios'
export type AreasProps = {
id?: string
name: string
desktopBanner: string | null
mobileBanner: string | null
}
export async function createAreas({
name,
desktopBanner,
mobileBanner,
}: AreasProps) {
const area = await api.post<AreasProps[]>('/areas', {
name,
desktopBanner,
mobileBanner,
})
return area
}
export async function editArea({
id,
name,
desktopBanner,
mobileBanner,
}: AreasProps) {
const area = await api.put<AreasProps[]>(`/areas/${id}`, {
name,
desktopBanner,
mobileBanner,
})
return area
}
export async function getAreas() {
const areas = await api.get<AreasProps[]>('/areas')
return areas
}
export async function getAreasId(id: string) {
const area = await api.get<AreasProps>(`/areas/${id}`)
return area
}
export async function deleteArea(id: string) {
const area = await api.delete(`/areas/${id}`)
return area
}
import { api } from '@/lib/axios'
export type AudiencesProps = {
id: string
name: string
}
export async function createAudiences({ name }: AudiencesProps) {
const audiences = await api.post<AudiencesProps>('/audiences', {
name,
})
return audiences
}
export async function getAudiences() {
const audiences = await api.get<AudiencesProps[]>('/audiences')
return audiences
}
export async function getAudienceId(id: string) {
const audiences = await api.get<AudiencesProps>(`/audiences/${id}`)
return audiences
}
export async function editAudienceId(data: AudiencesProps) {
const audiences = await api.put<AudiencesProps>(`/audiences/${data.id}`, {
name: data.name,
})
return audiences
}
export async function deleteAudience(id: string) {
const audiences = await api.delete(`/audiences/${id}`)
return audiences
}
import { api } from '@/lib/axios'
export type BannersProps = {
id: string
name: string
desktopBanner?: string
mobileBanner?: string
}
export async function getBanners() {
const banners = await api.get<BannersProps[]>('/banners')
return banners
}
import { api } from '@/lib/axios'
export type CategoriesProps = {
id: string
name: string
}
export async function createCategories({ name }: CategoriesProps) {
const categories = await api.post<CategoriesProps>('/categories', {
name,
})
return categories
}
export async function getCategories() {
const categories = await api.get<CategoriesProps[]>('/categories')
return categories
}
export async function getCategoryId(id: string) {
const categories = await api.get<CategoriesProps>(`/categories/${id}`)
return categories
}
export async function editCategoryId(data: CategoriesProps) {
const categories = await api.put<CategoriesProps>(`/categories/${data.id}`, {
name: data.name,
})
return categories
}
export async function deleteCategory(id: string) {
const categories = await api.delete(`/categories/${id}`)
return categories
}
import { api } from '@/lib/axios'
import { AreasProps } from './areas'
import { AudiencesProps } from './audiences'
import { CategoriesProps } from './categories'
import { ModulesProps } from './modules'
export type CourseIdProps = {
id?: string
name: string
area: AreasProps
audience: AudiencesProps
category: CategoriesProps
description: string
desktopBanner: string
mobileBanner: string
startDate?: string
endDate?: string
professors: string[]
workload: number
modules: {
courseId: string
module: ModulesProps
moduleId: string
}[]
}
export type CoursesProps = {
id?: string
name: string
description: string
desktopBanner: string | null
mobileBanner: string | null
startDate?: string
endDate?: string
professors: string[]
workload: number
areaId: string
audienceId: string
categoryId: string
area?: {
id: string
name: string
}
audience?: {
id: string
name: string
}
category?: {
id: string
name: string
}
moduleIds: string[]
}
export async function createCourses(data: CoursesProps) {
const course = await api.post<CoursesProps[]>('/courses', data)
return course
}
export async function getCourses() {
const courses = await api.get<CoursesProps[]>('/courses')
return courses
}
export async function getCourseId(id: string) {
const course = await api.get<CourseIdProps>(`/courses/${id}`)
return course
}
export async function editCourseId(data: CoursesProps) {
const course = await api.put<CoursesProps>(`/courses/${data.id}`, data)
return course
}
export async function deleteCourse(id: string) {
const course = await api.delete(`/courses/${id}`)
return course
}
import { api } from '@/lib/axios'
export type SignInProps = {
email: string
password: string
}
export type SignInResponse = {
access_token: string
user: {
id: string
name: string
email: string
}
}
export async function login(data: SignInProps) {
const token = await api.post<SignInResponse>('/login', data)
return token
}
import { api } from '@/lib/axios'
export type ModulesProps = {
id: string
name: string
description: string
}
export async function createModules({ name, description }: ModulesProps) {
const modules = await api.post<ModulesProps>('/modules', {
name,
description,
})
return modules
}
export async function editModule({ id, name, description }: ModulesProps) {
const modules = await api.patch<ModulesProps[]>(`/modules/${id}`, {
name,
description,
})
return modules
}
export async function getModules() {
const modules = await api.get<ModulesProps[]>('/modules')
return modules
}
export async function getModulesId(id: string) {
const modules = await api.get<ModulesProps>(`/modules/${id}`)
return modules
}
export async function deleteModule(id: string) {
const modules = await api.delete(`/modules/${id}`)
return modules
}
import { Search } from 'lucide-react' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { About } from '@/components/about' import { About } from '@/components/about'
...@@ -6,14 +7,16 @@ import { Banner } from '@/components/banner' ...@@ -6,14 +7,16 @@ import { Banner } from '@/components/banner'
import { CarouselComponent } from '@/components/corousel-component' import { CarouselComponent } from '@/components/corousel-component'
import { CourseCategory } from '@/components/course-category' import { CourseCategory } from '@/components/course-category'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { InputMui } from '@/components/mui/inputs'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Suspense } from 'react' import { Suspense } from 'react'
// import { useForm } from 'react-hook-form'
export default function Home() { export default function Home() {
// const { register } = useForm()
return ( return (
<> <>
<div className="absolute h-[200px] w-full bg-gradient-to-b from-gray-900 from-5% z-10" /> <div className="absolute h-[200px] w-full bg-gradient-to-b from-gray-900 from-5% z-10" />
...@@ -24,22 +27,25 @@ export default function Home() { ...@@ -24,22 +27,25 @@ export default function Home() {
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<div className="flex justify-center items-center w-full md:w-1/2 mx-auto mb-8"> {/* <div className="flex justify-center items-center w-full md:w-1/2 mx-auto mb-8">
<InputMui <InputMui
label="O que você quer aprender hoje?" label="O que você quer aprender hoje?"
variant="standard" variant="standard"
type="text" type="text"
className="w-full #fafafa" className="w-full"
themeColor="#fafafa"
name="learned-today"
register={register}
/> />
<Search size={24} /> <Search size={24} />
</div> </div> */}
<CarouselComponent /> <CarouselComponent />
<Button <Button
variant="secondary" variant="secondary"
className="uppercase mx-auto my-8" className="uppercase mx-auto my-8"
asChild asChild
> >
<Link href="#">Ver todos os cursos</Link> <Link href="/estudantes">Ver todos os cursos</Link>
</Button> </Button>
</div> </div>
<CourseCategory /> <CourseCategory />
......
'use client'
import { AreasProps, deleteArea, editArea, getAreasId } from '@/api/areas'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import InputFile from '@/components/input-file'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import ModalDialog from '@/components/modal-dialog'
import { StyledInputs } from '@/components/mui/styled-inputs' import { StyledInputs } from '@/components/mui/styled-inputs'
import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { TextField } from '@mui/material' import { TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Trash } from 'lucide-react' import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
export default function AreaId() { export default function AreaId() {
const [desktopBanner, setDesktopBanner] = useState<File | null>(null)
const [mobileBanner, setMobileBanner] = useState<File | null>(null)
const params = useParams<{ id: string }>()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AreasProps>()
const router = useRouter()
const queryClient = useQueryClient()
const { data: area, isLoading } = useQuery({
queryKey: ['areas', params.id],
queryFn: () => getAreasId(params.id),
enabled: !!params.id,
})
const mutationEdit = useMutation({
mutationFn: editArea,
onSuccess: () => {
toast.success('Área editada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['areas'] })
router.push('/admin')
},
})
const mutationDelete = useMutation({
mutationFn: deleteArea,
onSuccess: () => {
toast.success('Área deletada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['areas'] })
router.push('/admin')
},
})
async function onSubmit(data: AreasProps) {
const urlDesktop = await handleUpload(desktopBanner)
const urlMobile = await handleUpload(mobileBanner)
const dataArea = {
...data,
id: params.id,
desktopBanner: urlDesktop,
mobileBanner: urlMobile,
}
mutationEdit.mutateAsync(dataArea)
}
const handleUpload = async (file: File | null) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
function handleDeleteArea() {
mutationDelete.mutate(params.id)
}
if (isLoading) {
return (
<div className="flex w-full h-[500px] items-center justify-center">
<LoadingSpinIcon />
<span>Loading...</span>
</div>
)
}
return ( return (
<section className="container py-10"> <section className="container py-10">
<div className="flex items-center gap-6"> <BreadcrumbComponent page="Área" />
<h1 className="text-green-700 text-3xl font-bold">Editar Área</h1> <form onSubmit={handleSubmit(onSubmit)}>
<Button variant="third" className="uppercase"> <div className="flex items-center gap-6">
Salvar como rascunho <h1 className="text-3xl font-bold">Editar Área</h1>
</Button> <Button
<Button variant="secondary" className="uppercase"> variant="secondary"
Salvar e publicar type="submit"
</Button> className="uppercase rounded-sm"
<Button disabled={isLoading}
variant="ghost" >
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100" {mutationEdit.isPending && <LoadingSpinIcon />}
> Salvar e publicar
<span>Apagar área</span> </Button>
<Trash /> <AlertDialog>
</Button> <>
</div> <AlertDialogTrigger
<div className="w-full"> className="uppercase flex items-center gap-2 text-orange-100 border-none hover:bg-orange-100/10 hover:text-orange-100 h-10 px-5 rounded-sm"
<TextField type="button"
label="Nome da área" >
variant="standard" <span>Apagar área</span>
type="text" <Trash />
className="flex" </AlertDialogTrigger>
sx={StyledInputs({ color: '#26AAA7' })} <ModalDialog
/> title="Tem certeza que deseja deletar a área?"
description={`Ao clicar em continue você irá deletar a área ${area?.data.name}`}
<div className="flex justify-between flex-wrap gap-6"> handleClick={handleDeleteArea}
<div className="flex flex-col flex-1 mt-6"> />
<h6 className="text-xl text-purple-100">Banner Desktop</h6> </>
<span className="mt-4">Dimensões recomendadas: </span> </AlertDialog>
<span>Formatos aceitos: .png, .jpg</span> </div>
<div className="w-full mt-16">
<Button <TextField
variant="third" label="Nome da área"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full" type="text"
> defaultValue={area?.data.name}
Adicionar banner {...register('name')}
</Button> variant="standard"
</div> className="flex"
<div className="flex flex-col flex-1 mt-6"> sx={StyledInputs(
<h6 className="text-xl text-purple-100">Banner Mobile</h6> errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
<span className="mt-4">Dimensões recomendadas: </span> )}
<span>Formatos aceitos: .png, .jpg</span> />
<Button
variant="third" <div className="flex justify-between flex-wrap gap-6 py-6">
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full" <div className="flex flex-col flex-1 mt-6">
> <h6
Adicionar banner className={cn(
</Button> 'text-xl text-purple-100',
errors.desktopBanner && 'text-red-600',
)}
>
Banner Desktop
</h6>
<span className="mt-4">Dimensões recomendadas: 1512 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="desktopBanner"
label={!desktopBanner ? 'Atualizar banner' : desktopBanner.name}
onChange={(e) => setDesktopBanner(e.target.files?.[0] || null)}
/>
</div>
<div className="flex flex-col flex-1 mt-6">
<h6
className={cn(
'text-xl text-purple-100',
errors.mobileBanner && 'text-red-600',
)}
>
Banner Mobile
</h6>
<span className="mt-4">Dimensões recomendadas: 390 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="mobileBanner"
label={!mobileBanner ? 'Atualizar banner' : mobileBanner.name}
onChange={(e) => setMobileBanner(e.target.files?.[0] || null)}
/>
</div>
</div> </div>
</div> </div>
</div> </form>
</section> </section>
) )
} }
'use client'
import { AreasProps, createAreas } from '@/api/areas'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import InputFile from '@/components/input-file'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs' import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@mui/material' import { TextField } from '@mui/material'
import { Trash } from 'lucide-react' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Nome obrigatório' }),
})
export default function Area() { export default function Area() {
const [desktopBanner, setDesktopBanner] = useState<File | null>(null)
const [mobileBanner, setMobileBanner] = useState<File | null>(null)
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<AreasProps>({
resolver: zodResolver(FormSchema),
})
const router = useRouter()
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createAreas,
onSuccess: () => {
toast.success('Área criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['areas'] })
router.push('/admin')
},
})
async function onSubmit(data: AreasProps) {
if (!desktopBanner) {
setError('desktopBanner', {
type: 'manual',
message: 'Foto desktop obrigatória!',
})
return
}
if (!mobileBanner) {
setError('mobileBanner', {
type: 'manual',
message: 'Foto mobile obrigatória!',
})
return
}
const urlDesktop = await handleUpload(desktopBanner)
const urlMobile = await handleUpload(mobileBanner)
const dataArea = {
...data,
desktopBanner: urlDesktop,
mobileBanner: urlMobile,
}
mutation.mutateAsync(dataArea)
}
const handleUpload = async (file: File | null) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
return ( return (
<section className="container py-10"> <section className="container py-10">
<div className="flex items-center gap-6"> <BreadcrumbComponent page="Área" />
<h1 className="text-green-700 text-3xl font-bold">Criar Área</h1> <form onSubmit={handleSubmit(onSubmit)}>
<Button variant="third" className="uppercase"> <div className="flex items-center gap-6">
Salvar como rascunho <h1 className="text-3xl font-bold">Criar Área</h1>
</Button> <Button variant="secondary" className="uppercase rounded-sm">
<Button variant="secondary" className="uppercase"> {mutation.isPending && <LoadingSpinIcon />}
Salvar e publicar Salvar e publicar
</Button> </Button>
<Button </div>
variant="ghost" <div className="w-full mt-16">
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100" <TextField
> label="Nome da área"
<span>Apagar área</span> variant="standard"
<Trash /> type="text"
</Button> className="flex"
</div> {...register('name')}
<div className="w-full"> sx={StyledInputs(
<TextField errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
label="Nome da área" )}
variant="standard" />
type="text"
className="flex" <div className="flex justify-between flex-wrap gap-6 py-6">
sx={StyledInputs({ color: '#26AAA7' })} <div className="flex flex-col flex-1 mt-6">
/> <h6
className={cn(
<div className="flex justify-between flex-wrap gap-6"> 'text-xl text-purple-100',
<div className="flex flex-col flex-1 mt-6"> errors.desktopBanner && 'text-red-600',
<h6 className="text-xl text-purple-100">Banner Desktop</h6> )}
<span className="mt-4">Dimensões recomendadas: </span> >
<span>Formatos aceitos: .png, .jpg</span> Banner Desktop
</h6>
<Button <span className="mt-4">Dimensões recomendadas: 1512 x 300</span>
variant="third" <span>Formatos aceitos: .png, .jpg, .jpeg</span>
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
> <InputFile
Adicionar banner id="desktopBanner"
</Button> label={!desktopBanner ? 'Adicionar banner' : desktopBanner.name}
</div> onChange={(e) => setDesktopBanner(e.target.files?.[0] || null)}
<div className="flex flex-col flex-1 mt-6"> />
<h6 className="text-xl text-purple-100">Banner Mobile</h6> </div>
<span className="mt-4">Dimensões recomendadas: </span> <div className="flex flex-col flex-1 mt-6">
<span>Formatos aceitos: .png, .jpg</span> <h6
<Button className={cn(
variant="third" 'text-xl text-purple-100',
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full" errors.mobileBanner && 'text-red-600',
> )}
Adicionar banner >
</Button> Banner Mobile
</h6>
<span className="mt-4">Dimensões recomendadas: 390 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="mobileBanner"
label={!mobileBanner ? 'Adicionar banner' : mobileBanner.name}
onChange={(e) => setMobileBanner(e.target.files?.[0] || null)}
/>
</div>
</div> </div>
</div> </div>
</div> </form>
</section> </section>
) )
} }
'use client'
import {
AudiencesProps,
deleteAudience,
editAudienceId,
getAudienceId,
} from '@/api/audiences'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import ModalDialog from '@/components/modal-dialog'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
export default function AudienceId() {
const params = useParams<{ id: string }>()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AudiencesProps>()
const router = useRouter()
const queryClient = useQueryClient()
const { data: audience, isLoading } = useQuery({
queryKey: ['audiences', params.id],
queryFn: () => getAudienceId(params.id),
enabled: !!params.id,
})
const mutationEditAudience = useMutation({
mutationFn: editAudienceId,
onSuccess: () => {
toast.success('Audiência auterada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['audiences'] })
router.push('/admin')
},
})
const mutationDeleteAudience = useMutation({
mutationFn: deleteAudience,
onSuccess: () => {
toast.success('Audiência criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['audiences'] })
router.push('/admin')
},
})
function handleDeleteAudience() {
mutationDeleteAudience.mutate(params.id)
}
async function onSubmit(data: AudiencesProps) {
const body = {
id: params.id,
name: data.name,
}
mutationEditAudience.mutateAsync(body)
}
if (isLoading) {
return (
<div className="flex w-full h-[500px] items-center justify-center">
<LoadingSpinIcon />
<span>Loading...</span>
</div>
)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Audiência" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Editar Audiência</h1>
<Button
variant="secondary"
type="submit"
className="uppercase rounded-sm"
disabled={isLoading}
>
{mutationEditAudience.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
<AlertDialog>
<>
<AlertDialogTrigger
className="uppercase flex items-center gap-2 text-orange-100 border-none hover:bg-orange-100/10 hover:text-orange-100 h-10 px-5 rounded-sm"
type="button"
>
<span>Apagar audiência</span>
<Trash />
</AlertDialogTrigger>
<ModalDialog
title="Tem certeza que deseja deletar a audiência?"
description={`Ao clicar em continue você irá deletar a audiência ${audience?.data.name}`}
handleClick={handleDeleteAudience}
/>
</>
</AlertDialog>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da audiência"
type="text"
defaultValue={audience?.data.name}
{...register('name')}
variant="standard"
className="flex"
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client'
import { AudiencesProps, createAudiences } from '@/api/audiences'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@mui/material'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Nome obrigatório' }),
})
export default function Audience() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AudiencesProps>({
resolver: zodResolver(FormSchema),
})
const router = useRouter()
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createAudiences,
onSuccess: () => {
toast.success('Audiência criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['audiences'] })
router.push('/admin')
},
})
async function onSubmit(data: AudiencesProps) {
mutation.mutateAsync(data)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Audiência" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Criar Audiência</h1>
<Button variant="secondary" className="uppercase rounded-sm">
{mutation.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da audiência"
variant="standard"
type="text"
className="flex"
{...register('name')}
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client'
import {
CategoriesProps,
deleteCategory,
editCategoryId,
getCategoryId,
} from '@/api/categories'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import ModalDialog from '@/components/modal-dialog'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
export default function AudienceId() {
const params = useParams<{ id: string }>()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<CategoriesProps>()
const router = useRouter()
const queryClient = useQueryClient()
const { data: categorie, isLoading } = useQuery({
queryKey: ['categories', params.id],
queryFn: () => getCategoryId(params.id),
enabled: !!params.id,
})
const mutationEditAudience = useMutation({
mutationFn: editCategoryId,
onSuccess: () => {
toast.success('Categoria auterada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
router.push('/admin')
},
})
const mutationDeleteAudience = useMutation({
mutationFn: deleteCategory,
onSuccess: () => {
toast.success('Categoria criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
router.push('/admin')
},
})
function handleDeleteAudience() {
mutationDeleteAudience.mutate(params.id)
}
async function onSubmit(data: CategoriesProps) {
const body = {
id: params.id,
name: data.name,
}
mutationEditAudience.mutateAsync(body)
}
if (isLoading) {
return (
<div className="flex w-full h-[500px] items-center justify-center">
<LoadingSpinIcon />
<span>Loading...</span>
</div>
)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Categoria" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Editar Categoria</h1>
<Button
variant="secondary"
type="submit"
className="uppercase rounded-sm"
disabled={isLoading}
>
{mutationEditAudience.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
<AlertDialog>
<>
<AlertDialogTrigger
className="uppercase flex items-center gap-2 text-orange-100 border-none hover:bg-orange-100/10 hover:text-orange-100 h-10 px-5 rounded-sm"
type="button"
>
<span>Apagar categoria</span>
<Trash />
</AlertDialogTrigger>
<ModalDialog
title="Tem certeza que deseja deletar a categoria?"
description={`Ao clicar em continue você irá deletar a categoria ${categorie?.data.name}`}
handleClick={handleDeleteAudience}
/>
</>
</AlertDialog>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da categoria"
type="text"
defaultValue={categorie?.data.name}
{...register('name')}
variant="standard"
className="flex"
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client'
import { CategoriesProps, createCategories } from '@/api/categories'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@mui/material'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Nome obrigatório' }),
})
export default function Category() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<CategoriesProps>({
resolver: zodResolver(FormSchema),
})
const router = useRouter()
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createCategories,
onSuccess: () => {
toast.success('Categoria criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
router.push('/admin')
},
})
async function onSubmit(data: CategoriesProps) {
mutation.mutateAsync(data)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Categoria" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Criar Categoria</h1>
<Button variant="secondary" className="uppercase rounded-sm">
{mutation.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da categoria"
variant="standard"
type="text"
className="flex"
{...register('name')}
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
This diff is collapsed.
import { StyledInputs } from '@/components/mui/styled-inputs'
import { TextField } from '@mui/material'
import { useForm } from 'react-hook-form'
type FormModulesProps = {
index?: number
remove?: (index: number | number[]) => void
}
export function FormNewModules({ index, remove }: FormModulesProps) {
const { register } = useForm()
return (
<div className="flex flex-col p-4 gap-6 border border-gray-100">
<TextField
label="Título"
variant="standard"
type="text"
sx={StyledInputs({ color: '#26AAA7' })}
{...register(`newModule.${index}.name`)}
/>
<TextField
label="Descrição"
variant="standard"
type="text"
multiline
rows={4}
sx={StyledInputs({ color: '#26AAA7' })}
{...register(`newModule.${index}.description`)}
/>
</div>
)
}
This diff is collapsed.
'use client'
import { getAreas } from '@/api/areas'
import { getAudiences } from '@/api/audiences'
import { getCategories } from '@/api/categories'
import { getCourses } from '@/api/courses'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { InputMui } from '@/components/mui/inputs' import { InputMui } from '@/components/mui/inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { CoursesCard } from '@/utils/courses-array' import { AuthContext } from '@/contexts/auth-context'
import { useQuery } from '@tanstack/react-query'
import { Pencil, Search } from 'lucide-react' import { Pencil, Search } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { useContext, useEffect, useState } from 'react'
const areas = [ import { useForm } from 'react-hook-form'
{
id: '1',
title: 'Educação',
},
{
id: '2',
title: 'Negócios',
},
{
id: '3',
title: 'Comunicação',
},
{
id: '4',
title: 'Saúde',
},
{
id: '5',
title: 'Tecnologia',
},
{
id: '6',
title: 'Teologia',
},
]
const banners = [
{
title: 'Banner Home',
},
{
title: 'CTAs',
},
]
export default function Admin() { export default function Admin() {
const { register, watch } = useForm()
const { user } = useContext(AuthContext)
const search = watch('search', '')
const [debouncedSearch, setDebouncedSearch] = useState('')
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(search)
}, 500)
return () => clearTimeout(handler)
}, [search])
const {
data: areas,
error,
isLoading: areasLoading,
isError: areasIsError,
} = useQuery({ queryKey: ['areas'], queryFn: getAreas })
const { data: categories } = useQuery({
queryKey: ['categories'],
queryFn: getCategories,
})
const {
data: audiences,
isLoading: audiencesLoading,
isError: audiencesIsError,
} = useQuery({
queryKey: ['audiences'],
queryFn: getAudiences,
})
const {
data: courses,
isLoading: coursesLoading,
isError: coursesIsError,
} = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filteredCourses = courses?.data.filter((course) =>
course.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
if (areasLoading || coursesLoading || audiencesLoading) {
return <span>Loading...</span>
}
if (areasIsError || audiencesIsError || coursesIsError) {
return <span>Error: {error?.message}</span>
}
return ( return (
<section className="container"> <section className="container">
<h1 className="text-green-400 text-2xl">Bem vindo, João da Silva,</h1> <h1 className="text-green-400 text-2xl">Bem vindo, {user?.name}!</h1>
<div className="flex h-auto mb-20"> <div className="flex h-auto mb-20">
<aside className="mt-10 w-[348px]"> <aside className="mt-10 w-[348px]">
<h2 className="text-purple-50 text-2xl">Áreas</h2> <h2 className="text-purple-50 text-2xl">Áreas</h2>
...@@ -55,13 +84,13 @@ export default function Admin() { ...@@ -55,13 +84,13 @@ export default function Admin() {
<nav className="my-6"> <nav className="my-6">
<ul className="space-y-4"> <ul className="space-y-4">
{areas.map((area) => ( {areas?.data.map((area) => (
<li key={area.title} className="border-b border-green-400"> <li key={area.id} className="border-b border-green-400">
<Link <Link
href={`/admin/area/${area.id}`} href={`/admin/area/${area.id}`}
className="flex justify-between py-2 hover:bg-green-400/10" className="flex justify-between py-2 hover:bg-green-400/10"
> >
<span>{area.title}</span> <span>{area.name}</span>
<Pencil className="text-green-400" /> <Pencil className="text-green-400" />
</Link> </Link>
</li> </li>
...@@ -69,16 +98,45 @@ export default function Admin() { ...@@ -69,16 +98,45 @@ export default function Admin() {
</ul> </ul>
</nav> </nav>
<h2 className="text-purple-50 text-2xl mt-10">Banners</h2> <h2 className="text-purple-50 text-2xl mt-10">Audiência</h2>
<Button className="uppercase mt-6" asChild>
<Link href="admin/audience">+ Adicionar novo</Link>
</Button>
<nav className="my-6"> <nav className="my-6">
<ul className="space-y-4"> <ul className="space-y-4">
{banners.map((banner) => ( {audiences?.data.map((audience) => (
<li <li key={audience.id} className="border-b border-green-400">
key={banner.title} <Link
className="flex justify-between border-b border-green-400 py-2" href={`/admin/audience/${audience.id}`}
> className="flex justify-between py-2 hover:bg-green-400/10"
<Link href="#">{banner.title}</Link> >
<Pencil className="text-green-400" /> <span>{audience.name}</span>
<Pencil className="text-green-400" />
</Link>
</li>
))}
</ul>
</nav>
<h2 className="text-purple-50 text-2xl mt-10">Categoria</h2>
<Button className="uppercase mt-6" asChild>
<Link href="admin/category">+ Adicionar novo</Link>
</Button>
<nav className="my-6">
<ul className="space-y-4">
{categories?.data.map((category) => (
<li key={category.id} className="border-b border-green-400">
<Link
href={`/admin/category/${category.id}`}
className="flex justify-between py-2 hover:bg-green-400/10"
>
<span>{category.name}</span>
<Pencil className="text-green-400" />
</Link>
</li> </li>
))} ))}
</ul> </ul>
...@@ -96,31 +154,45 @@ export default function Admin() { ...@@ -96,31 +154,45 @@ export default function Admin() {
type="text" type="text"
variant="standard" variant="standard"
className="w-full" className="w-full"
themeColor="#fafafa"
name="search"
register={register}
/> />
<Search size={24} className="-ml-6" /> <Search size={24} className="-ml-6" />
</div> </div>
<div className="flex flex-wrap justify-around gap-4"> {courses?.data.length === 0 ? (
{CoursesCard.map((course) => ( <p className="text-yellow-100">Não há curso cadastrado!</p>
<Link ) : (
key={course.id} <div className="flex flex-wrap justify-around gap-4">
className="lowercase scale-100 hover:scale-105 duration-300" {filteredCourses?.map((course) => (
href={`admin/curso/${course.id}`} <Link
> key={course.id}
<div className="border border-gray-100 pb-4 rounded-lg overflow-hidden"> className="lowercase scale-100 hover:scale-105 duration-300"
<Card.Image image={course.image.src} width={240} height={320}> href={`admin/curso/${course.id}`}
<Card.Title title={course.title} /> >
</Card.Image> <div className="border border-gray-100 pb-4 rounded-lg overflow-hidden">
<Button <Card.Image
variant="secondary" image={course.mobileBanner || ''}
className="flex gap-4 mt-4 mx-auto" width={240}
> height={320}
<Pencil /> >
<span className="uppercase">Editar</span> <Card.Title title={course.name} />
</Button> </Card.Image>
</div> <Button
</Link> variant="secondary"
))} className="flex gap-4 mt-4 mx-auto"
</div> >
<Pencil />
<span className="uppercase">Editar</span>
</Button>
</div>
</Link>
))}
{filteredCourses?.length === 0 && (
<p className="text-yellow-100">Nenhum curso encontrado!</p>
)}
</div>
)}
</div> </div>
</div> </div>
</section> </section>
......
import { NextResponse } from 'next/server'
import nodemailer from 'nodemailer'
export async function POST(req: Request) {
try {
const { fullname, email, subject, message } = await req.json()
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: Number(process.env.EMAIL_PORT),
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
})
const mailOptions = {
from: email,
to: process.env.EMAIL_SEVENPRO,
subject: `Nova dúvida de ${fullname}: ${subject}`,
text: message,
html: `<p><strong>Nome:</strong> ${fullname}</p><p><strong>Assunto:</strong> ${subject}</p><p><strong>Mensagem:</strong></p><p>${message}</p>`,
}
await transporter.sendMail(mailOptions)
return NextResponse.json(
{ message: 'E-mail enviado com sucesso!' },
{ status: 200 },
)
} catch (error) {
return NextResponse.json(
{ error: 'Erro ao enviar o e-mail.' },
{ status: 500 },
)
}
}
import { writeFile } from 'fs/promises'
import path from 'path'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
}
try {
const arrayBuffer = await file.arrayBuffer()
const buffer = new Uint8Array(arrayBuffer)
const fileName = `${Date.now()}-${file.name}`
const filePath = path.join(process.cwd(), 'public', 'imgs', fileName)
await writeFile(filePath, buffer)
const fileUrl = `/imgs/${fileName}` // caminho relativo acessível pela tag <img src=...>
return NextResponse.json({ url: fileUrl })
} catch (error) {
console.error('Upload failed:', error)
return NextResponse.json({ error: 'Upload failed' }, { status: 500 })
}
}
'use client'
import { InputMui } from '@/components/mui/inputs' import { InputMui } from '@/components/mui/inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { MenuItem } from '@mui/material' import { MenuItem } from '@mui/material'
import { Clock4, Mail, Smartphone } from 'lucide-react' import { Clock4, Mail, Smartphone } from 'lucide-react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
const matters = [ const matters = [
{ {
...@@ -22,7 +28,57 @@ const matters = [ ...@@ -22,7 +28,57 @@ const matters = [
}, },
] ]
type DataEmail = {
fullname: string
email: string
message: string
subject: string
}
const schema = z
.object({
fullname: z.string().min(5, { message: 'Digite seu nome completo' }),
email: z.string().email({ message: 'E-mail inválido' }),
message: z
.string()
.min(5, { message: 'Mensagem deve ter no mínimo 5 caracteres' }),
subject: z.string().min(2, { message: 'Selecione uma assunto' }),
})
.required()
export default function Contact() { export default function Contact() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<DataEmail>({
resolver: zodResolver(schema),
})
const [status, setStatus] = useState('')
async function onSubmit(data: DataEmail) {
setStatus('Enviando...')
try {
const response = await fetch('/api/send-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (response.ok) {
setStatus('E-mail enviado com sucesso!')
reset()
} else {
setStatus('Erro ao enviar o e-mail.')
}
} catch (error) {
setStatus('Erro ao enviar o e-mail.')
}
}
return ( return (
<main className="container flex flex-col justify-center items-center py-20"> <main className="container flex flex-col justify-center items-center py-20">
<h1 className="text-center font-normal md:font-extrabold text-green-800 text-2xl md:text-4xl"> <h1 className="text-center font-normal md:font-extrabold text-green-800 text-2xl md:text-4xl">
...@@ -36,15 +92,15 @@ export default function Contact() { ...@@ -36,15 +92,15 @@ export default function Contact() {
<ul className="flex flex-col gap-4 mt-6"> <ul className="flex flex-col gap-4 mt-6">
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
<Clock4 className="text-green-800" /> <Clock4 className="text-green-800" />
<span>De XXh à XXh</span> <span>De segunda à sexta das 08h às 18h</span>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
<Smartphone className="text-green-800" /> <Smartphone className="text-green-800" />
<span>Telefones: (XX) XXXX-XXXX</span> <span>Telefones: (44) 9725-0427</span>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
<Mail className="text-green-800" /> <Mail className="text-green-800" />
<span>E-mail: sevenpro@sevenpro.com.br</span> <span>E-mail: id.sevenpro@gmail.com</span>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -52,23 +108,59 @@ export default function Contact() { ...@@ -52,23 +108,59 @@ export default function Contact() {
<h2 className="text-green-800 text-xl md:text-2xl"> <h2 className="text-green-800 text-xl md:text-2xl">
Envie uma mensagem Envie uma mensagem
</h2> </h2>
<form action="" className="mt-6 flex flex-col gap-6"> <form
<InputMui type="text" variant="outlined" label="Nome completo" /> onSubmit={handleSubmit(onSubmit)}
className="mt-6 flex flex-col gap-6"
>
<InputMui
type="text"
variant="outlined"
label="Nome completo"
name="fullname"
register={register}
/>
{errors.fullname?.message && (
<p className="text-red-600">{errors.fullname?.message}</p>
)}
<InputMui
type="text"
variant="outlined"
label="E-mail"
name="email"
register={register}
/>
{errors.email?.message && (
<p className="text-red-600">{errors.email?.message}</p>
)}
<InputMui <InputMui
type="text" type="text"
variant="outlined" variant="outlined"
label="Mensagem" label="Mensagem"
multiline multiline
rows={4} rows={4}
name="message"
register={register}
/> />
{errors.message?.message && (
<InputMui type="text" variant="outlined" label="Assunto" select> <p className="text-red-600">{errors.message?.message}</p>
)}
<InputMui
type="text"
variant="outlined"
label="Assunto"
select
name="subject"
register={register}
>
{matters.map((option) => ( {matters.map((option) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
{option.label} {option.label}
</MenuItem> </MenuItem>
))} ))}
</InputMui> </InputMui>
{errors.subject?.message && (
<p className="text-red-600">{errors.subject?.message}</p>
)}
<div className="flex"> <div className="flex">
<Button type="submit" className="w-auto"> <Button type="submit" className="w-auto">
...@@ -76,6 +168,7 @@ export default function Contact() { ...@@ -76,6 +168,7 @@ export default function Contact() {
</Button> </Button>
</div> </div>
</form> </form>
{status && <p className="mt-4 text-sm text-white">{status}</p>}
</div> </div>
</div> </div>
</main> </main>
......
'use client'
import Image from 'next/image' import Image from 'next/image'
import { getCourseId } from '@/api/courses'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { CarouselComponent } from '@/components/corousel-component' import { CarouselComponent } from '@/components/corousel-component'
import { CourseDetails } from '@/components/course-details' import { CourseDetails } from '@/components/course-details'
...@@ -11,19 +14,35 @@ import { ...@@ -11,19 +14,35 @@ import {
} from '@/components/ui/accordion' } from '@/components/ui/accordion'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'next/navigation'
import imageCapa from '../../../../public/images/banner.png' import imageCapa from '../../../../public/images/banner.png'
export default function CoursePage() { export default function CoursePage() {
const params = useParams<{ courseId: string }>()
const { data: course, isLoading } = useQuery({
queryKey: ['course', params.courseId],
queryFn: () => getCourseId(params.courseId),
enabled: !!params.courseId,
})
if (isLoading) {
return <p>Carregando...</p>
}
return ( return (
<> <>
<BannerCategory /> <BannerCategory data={course?.data.area.name} />
<div className="flex flex-col md:flex-row px-6 pt-20 gap-6"> <div className="flex flex-col md:flex-row px-6 pt-20 gap-6">
<div className="flex flex-col gap-7"> <div className="flex flex-col gap-7">
<h1 className="block md:hidden text-green-400 text-3xl font-thin"> <h1 className="block md:hidden text-green-400 text-3xl font-thin">
História Contemporânea dos Estados Unidos {course?.data.name}
</h1> </h1>
<Image <Image
src={imageCapa} src={course?.data.mobileBanner || imageCapa}
width={500}
height={500}
className="min-w-[348px] h-[464px] object-cover rounded-lg" className="min-w-[348px] h-[464px] object-cover rounded-lg"
alt="Imagem de capa do curso" alt="Imagem de capa do curso"
/> />
...@@ -33,43 +52,43 @@ export default function CoursePage() { ...@@ -33,43 +52,43 @@ export default function CoursePage() {
</div> </div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<h1 className="hidden md:block text-green-400 text-4xl font-extrabold"> <h1 className="hidden md:block text-green-400 text-4xl font-extrabold">
História Contemporânea dos Estados Unidos {course?.data.name}
</h1> </h1>
<div className="flex flex-col-reverse lg:flex-row gap-6 w-full"> <div className="flex flex-col-reverse lg:flex-row gap-6 w-full">
<div className="flex flex-col lg:w-1/2"> <div className="flex flex-col lg:w-1/2">
<h3 className="font-bold">O que você vai aprender</h3> <h3 className="font-bold">O que você vai aprender</h3>
<p> <p>{course?.data.description}</p>
Este curso levará você por uma jornada desde os tumultuados anos
de 1970 até os dias atuais, explorando os impactos decisivos dos
governos de Bush, Obama, Trump e Biden. Descubra os
momentos-chave que moldaram a história contemporânea dos Estados
Unidos, desde a resiliência diante de crises até as mudanças
geopolíticas e sociais. Vamos desvendar os bastidores dos
eventos que marcaram época, proporcionando uma compreensão
profunda e contextualizada da evolução norte-americana.
Prepare-se para uma experiência de aprendizado envolvente e
reveladora, onde o passado ilumina o presente, moldando o
futuro.
</p>
<h3 className="font-bold mt-6">Com quem você vai aprender</h3> <h3 className="font-bold mt-6">Com quem você vai aprender</h3>
<p>Professor João Silva</p> {course?.data.professors &&
course?.data.professors.length > 0 && (
<p>{course?.data.professors.join(', ')}</p>
)}
</div> </div>
<div className="lg:w-1/2"> <div className="lg:w-1/2">
<CourseDetails /> <CourseDetails data={course?.data} />
</div> </div>
</div> </div>
<Button variant="secondary" className="uppercase block md:hidden"> <Button variant="secondary" className="uppercase block md:hidden">
Quero este curso Quero este curso
</Button> </Button>
<span className="font-bold mt-4 block md:hidden">Saiba mais:</span> <span className="font-bold mt-4 block md:hidden">Saiba mais:</span>
<Accordion type="single" collapsible className="w-full"> {course?.data.modules
<AccordionItem value="item-1"> .sort((a, b) => a.module.name.localeCompare(b.module.name))
<AccordionTrigger>Módulos</AccordionTrigger> .map((module) => (
<AccordionContent> <Accordion
Yes. It adheres to the WAI-ARIA design pattern. type="single"
</AccordionContent> collapsible
</AccordionItem> className="w-full"
</Accordion> key={module.module.id}
>
<AccordionItem value={module.module.id}>
<AccordionTrigger>{module.module.name}</AccordionTrigger>
<AccordionContent>
{module.module.description}
</AccordionContent>
</AccordionItem>
</Accordion>
))}
</div> </div>
</div> </div>
<div className="my-10 px-6"> <div className="my-10 px-6">
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about' import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { PaginationComponent } from '@/components/pagination-component'
import SearchFilter from '@/components/search-filter'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { CoursesCard } from '@/utils/courses-array' import { formatDate } from '@/utils/formatDate'
import { useQuery } from '@tanstack/react-query'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
export default function Companies() { export default function Companies() {
const path = usePathname()
const searchParams = useSearchParams()
const category = searchParams.get('area')
const { data: courses, isLoading: coursesLoading } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filterCoursesAudience = courses?.data.filter((item) =>
path.toLowerCase().includes(item?.audience?.name.toLowerCase() as string),
)
const filterCoursesAudiencesAndArea = filterCoursesAudience?.filter((area) =>
category?.includes(area?.area?.name.toLowerCase() as string),
)
if (coursesLoading) {
return <p>Carregando...</p>
}
return ( return (
<main> <main>
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory /> <BannerCategory />
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<SearchFilter /> {/* <SearchFilter /> */}
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6"> <div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6">
{CoursesCard.map((course) => ( {!category &&
<div className="flex justify-center" key={course.id}> filterCoursesAudience &&
<Card.Root link={`curso/${course.id}`}> filterCoursesAudience.map((course) => (
<Card.Image image={course.image.src} width={273} height={365}> <div className="flex justify-center" key={course.id}>
<Card.Title title={course.title} /> <Card.Root link={`/curso/${course.id}`}>
</Card.Image> <Card.Image
<Card.Content description={course.hours}> image={course.mobileBanner as string}
<Card.Icon icon={Clock4} /> width={273}
</Card.Content> height={365}
<Card.Content description={course.category}> >
<Card.Icon icon={User} /> <Card.Title title={course.name} />
</Card.Content> </Card.Image>
<Card.Content description={course.calender}> <Card.Content
<Card.Icon icon={Calendar} /> description={`${course.workload} hrs / ${course.category?.name}`}
</Card.Content> >
</Card.Root> <Card.Icon icon={Clock4} />
</div> </Card.Content>
))} <Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div>
))}
{category &&
filterCoursesAudiencesAndArea &&
filterCoursesAudiencesAndArea.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div>
))}
{category && filterCoursesAudiencesAndArea?.length === 0 && (
<p>
Não há cursos para a área <b>{category}</b> com audiência{' '}
<b>{path.replace('/', '')}</b>
</p>
)}
{!category && filterCoursesAudience?.length === 0 && (
<p>
Não há cursos para a audiência <b>{path.replace('/', '')}</b>
</p>
)}
</div> </div>
<div className="my-6"> {/* <div className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} /> <PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> </div> */}
<Differences /> <Differences />
<SignUp /> <SignUp />
<About.Root> <About.Root>
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about' import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { PaginationComponent } from '@/components/pagination-component'
import SearchFilter from '@/components/search-filter'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { CoursesCard } from '@/utils/courses-array' import { formatDate } from '@/utils/formatDate'
import { useQuery } from '@tanstack/react-query'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
export default function Students() { export default function Students() {
const path = usePathname()
const searchParams = useSearchParams()
const category = searchParams.get('area')
const { data: courses, isLoading: coursesLoading } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filterCoursesAudience = courses?.data.filter((item) =>
path.toLowerCase().includes(item?.audience?.name.toLowerCase() as string),
)
const filterCoursesAudiencesAndArea = filterCoursesAudience?.filter((area) =>
category?.includes(area?.area?.name.toLowerCase() as string),
)
if (coursesLoading) {
return <p>Carregando...</p>
}
return ( return (
<main> <main>
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory /> <BannerCategory />
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<SearchFilter /> {/* <SearchFilter /> */}
<div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6"> <div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6">
{CoursesCard.map((course) => ( {!category &&
<div className="flex justify-center" key={course.id}> filterCoursesAudience &&
<Card.Root link={`curso/${course.id}`}> filterCoursesAudience.map((course) => (
<Card.Image image={course.image.src} width={273} height={365}> <div className="flex justify-center" key={course.id}>
<Card.Title title={course.title} /> <Card.Root link={`/curso/${course.id}`}>
</Card.Image> <Card.Image
<Card.Content description={course.hours}> image={course.mobileBanner as string}
<Card.Icon icon={Clock4} /> width={273}
</Card.Content> height={365}
<Card.Content description={course.category}> >
<Card.Icon icon={User} /> <Card.Title title={course.name} />
</Card.Content> </Card.Image>
<Card.Content description={course.calender}> <Card.Content
<Card.Icon icon={Calendar} /> description={`${course.workload} hrs / ${course.category?.name}`}
</Card.Content> >
</Card.Root> <Card.Icon icon={Clock4} />
</div> </Card.Content>
))} <Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div>
))}
{category &&
filterCoursesAudiencesAndArea &&
filterCoursesAudiencesAndArea.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div>
))}
{category && filterCoursesAudiencesAndArea?.length === 0 && (
<p>
Não há cursos para a área <b>{category}</b> com audiência{' '}
<b>{path.replace('/', '')}</b>
</p>
)}
{!category && filterCoursesAudience?.length === 0 && (
<p>
Não há cursos para a audiência <b>{path.replace('/', '')}</b>
</p>
)}
</div> </div>
<div className="my-6"> {/* <div className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} /> <PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> </div> */}
<Differences /> <Differences />
<SignUp /> <SignUp />
<About.Root> <About.Root>
......
...@@ -5,6 +5,10 @@ import '@/styles/globals.css' ...@@ -5,6 +5,10 @@ import '@/styles/globals.css'
import { StyledEngineProvider } from '@mui/material' import { StyledEngineProvider } from '@mui/material'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { Poppins } from 'next/font/google' import { Poppins } from 'next/font/google'
import { Toaster } from 'sonner'
import { AuthProvider } from '@/contexts/auth-context'
import ReactQueryProvider from '@/providers/react-query'
const poppins = Poppins({ subsets: ['latin'], weight: ['300', '400', '800'] }) const poppins = Poppins({ subsets: ['latin'], weight: ['300', '400', '800'] })
...@@ -21,18 +25,23 @@ export default function RootLayout({ ...@@ -21,18 +25,23 @@ export default function RootLayout({
return ( return (
<html lang="pt-BR"> <html lang="pt-BR">
<body className={`antialiased ${poppins.className}`}> <body className={`antialiased ${poppins.className}`}>
<ThemeProvider <AuthProvider>
attribute="class" <ThemeProvider
defaultTheme="dark" attribute="class"
enableSystem defaultTheme="dark"
disableTransitionOnChange enableSystem
> disableTransitionOnChange
<StyledEngineProvider injectFirst> >
<Header /> <StyledEngineProvider injectFirst>
{children} <ReactQueryProvider>
<Footer /> <Toaster richColors />
</StyledEngineProvider> <Header />
</ThemeProvider> {children}
<Footer />
</ReactQueryProvider>
</StyledEngineProvider>
</ThemeProvider>
</AuthProvider>
</body> </body>
</html> </html>
) )
......
'use client'
import { InputMui } from '@/components/mui/inputs' import { InputMui } from '@/components/mui/inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { AuthContext } from '@/contexts/auth-context'
import { AxiosError } from 'axios'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useContext } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import globo from '../../../public/images/globo.svg' import globo from '../../../public/images/globo.svg'
type FormLogin = {
email: string
password: string
}
export default function Login() { export default function Login() {
const { handleSubmit, register } = useForm<FormLogin>()
const { signIn } = useContext(AuthContext)
const router = useRouter()
const onSubmit = async (data: FormLogin) => {
try {
await signIn(data)
router.push('/admin')
} catch (error) {
if (error instanceof AxiosError) {
const { message } = error.response?.data
toast.error(`${message}`)
}
}
}
return ( return (
<main> <main>
<section className=" container flex flex-col justify-center items-center space-y-10 h-[723px]"> <section className=" container flex flex-col justify-center items-center space-y-10 h-[723px]">
...@@ -20,15 +48,27 @@ export default function Login() { ...@@ -20,15 +48,27 @@ export default function Login() {
Acesso exclusivo para usuários cadastrados. Acesso exclusivo para usuários cadastrados.
</h2> </h2>
<form <form
// onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full md:w-[600px] flex flex-col gap-8 order-4" className="w-full md:w-[600px] flex flex-col gap-8 order-4"
> >
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<InputMui label="E-mail" variant="outlined" type="email" /> <InputMui
<InputMui label="Senha" variant="outlined" type="password" /> label="E-mail"
variant="outlined"
type="email"
name="email"
register={register}
/>
<InputMui
label="Senha"
variant="outlined"
type="password"
name="password"
register={register}
/>
</div> </div>
<div className="flex items-center space-x-2"> {/* <div className="flex items-center space-x-2">
<Checkbox id="remember" /> <Checkbox id="remember" />
<label <label
htmlFor="remember" htmlFor="remember"
...@@ -36,9 +76,10 @@ export default function Login() { ...@@ -36,9 +76,10 @@ export default function Login() {
> >
Lembrar-me Lembrar-me
</label> </label>
</div> </div> */}
<Button className="rounded-sm" asChild> <Button className="rounded-sm" type="submit">
<Link href="/admin">Entrar</Link> {/* <Link href="/admin">Entrar</Link> */}
Entrar
</Button> </Button>
<Button <Button
variant={'link'} variant={'link'}
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about' import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { PaginationComponent } from '@/components/pagination-component'
import SearchFilter from '@/components/search-filter'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { CoursesCard } from '@/utils/courses-array' import { formatDate } from '@/utils/formatDate'
import { useQuery } from '@tanstack/react-query'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
export default function Professionals() { export default function Professionals() {
const path = usePathname()
const searchParams = useSearchParams()
const category = searchParams.get('area')
const { data: courses, isLoading: coursesLoading } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filterCoursesAudience = courses?.data.filter((item) =>
path.toLowerCase().includes(item?.audience?.name.toLowerCase() as string),
)
const filterCoursesAudiencesAndArea = filterCoursesAudience?.filter((area) =>
category?.includes(area?.area?.name.toLowerCase() as string),
)
if (coursesLoading) {
return <p>Carregando...</p>
}
return ( return (
<main> <main>
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory /> <BannerCategory />
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<SearchFilter /> {/* <SearchFilter /> */}
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6"> <div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6">
{CoursesCard.map((course) => ( {!category &&
<div className="flex justify-center" key={course.id}> filterCoursesAudience &&
<Card.Root link={`curso/${course.id}`}> filterCoursesAudience.map((course) => (
<Card.Image image={course.image.src} width={273} height={365}> <div className="flex justify-center" key={course.id}>
<Card.Title title={course.title} /> <Card.Root link={`/curso/${course.id}`}>
</Card.Image> <Card.Image
<Card.Content description={course.hours}> image={course.mobileBanner as string}
<Card.Icon icon={Clock4} /> width={273}
</Card.Content> height={365}
<Card.Content description={course.category}> >
<Card.Icon icon={User} /> <Card.Title title={course.name} />
</Card.Content> </Card.Image>
<Card.Content description={course.calender}> <Card.Content
<Card.Icon icon={Calendar} /> description={`${course.workload} hrs / ${course.category?.name}`}
</Card.Content> >
</Card.Root> <Card.Icon icon={Clock4} />
</div> </Card.Content>
))} <Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div>
))}
{category &&
filterCoursesAudiencesAndArea &&
filterCoursesAudiencesAndArea.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div>
))}
{category && filterCoursesAudiencesAndArea?.length === 0 && (
<p>
Não há cursos para a área <b>{category}</b> com audiência{' '}
<b>{path.replace('/', '')}</b>
</p>
)}
{!category && filterCoursesAudience?.length === 0 && (
<p>
Não há cursos para a audiência <b>{path.replace('/', '')}</b>
</p>
)}
</div> </div>
<div className="my-6"> {/* <div className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} /> <PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> </div> */}
<Differences /> <Differences />
<SignUp /> <SignUp />
<About.Root> <About.Root>
......
...@@ -2,76 +2,49 @@ ...@@ -2,76 +2,49 @@
import Image from 'next/image' import Image from 'next/image'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import bannerComunicacao from '../../public/images/banners/comunicacao.jpg'
import bannerEducacao from '../../public/images/banners/educacao.jpg'
import bannerNegocios from '../../public/images/banners/negocios.jpg'
import bannerSaude from '../../public/images/banners/saude.jpg'
import bannerStudants from '../../public/images/banners/students_banner.jpg' import bannerStudants from '../../public/images/banners/students_banner.jpg'
import bannerTecnologia from '../../public/images/banners/tecnologia.jpg'
import bannerTeologia from '../../public/images/banners/teologia.jpg'
import bannerComunicacaoMobile from '../../public/images/banners/comunicacao-mobile.jpg' import { getAreas } from '@/api/areas'
import bannerEducacaoMobile from '../../public/images/banners/educacao-mobile.jpg' import { useQuery } from '@tanstack/react-query'
import bannerNegociosMobile from '../../public/images/banners/negocios-mobile.jpg'
import bannerSaudeMobile from '../../public/images/banners/saude-mobile.jpg'
import bannerTecnologiaMobile from '../../public/images/banners/tecnologia-mobile.jpg'
import bannerTeologiaMobile from '../../public/images/banners/teologia-mobile.jpg'
export function BannerCategory() { export function BannerCategory(areaBanner: { data?: string | undefined }) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const category = searchParams.get('categoria') const category = searchParams.get('area')
const { data: areas } = useQuery({ queryKey: ['areas'], queryFn: getAreas })
function desktopBannerCategory() { const bannerImage = areas?.data.find(
switch (category) { (item) => item.name.toLowerCase() === category?.toLowerCase(),
case 'tecnologia': )
return bannerTecnologia
case 'negocios':
return bannerNegocios
case 'comunicacao':
return bannerComunicacao
case 'educacao':
return bannerEducacao
case 'saude':
return bannerSaude
case 'teologia':
return bannerTeologia
default:
return bannerStudants
}
}
function mobileBannerCategory() { const bannerImageArea = areas?.data.find(
switch (category) { (item) => item.name.toLowerCase() === areaBanner?.data?.toLowerCase(),
case 'tecnologia': )
return bannerTecnologiaMobile
case 'negocios':
return bannerNegociosMobile
case 'comunicacao':
return bannerComunicacaoMobile
case 'educacao':
return bannerEducacaoMobile
case 'saude':
return bannerSaudeMobile
case 'teologia':
return bannerTeologiaMobile
default:
return bannerStudants
}
}
return ( return (
<div className="h-[300px] flex items-center justify-end"> <div className="h-[300px] flex items-center justify-end">
<Image <Image
alt="banner" alt="banner"
className="hidden md:flex w-full h-full object-cover" className="hidden md:flex w-full h-full object-cover"
src={desktopBannerCategory()} width={100}
height={100}
src={
bannerImage?.desktopBanner ||
bannerImageArea?.desktopBanner ||
bannerStudants
}
unoptimized unoptimized
/> />
<Image <Image
alt="banner" alt="banner"
className="flex md:hidden w-full h-full object-cover" className="flex md:hidden w-full h-full object-cover"
src={mobileBannerCategory()} width={100}
height={100}
src={
bannerImage?.mobileBanner ||
bannerImageArea?.mobileBanner ||
bannerStudants
}
unoptimized unoptimized
/> />
</div> </div>
......
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import Link from 'next/link'
type BreadcrumbProps = {
page: string
}
export default function BreadcrumbComponent({ page }: BreadcrumbProps) {
return (
<Breadcrumb className="my-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/admin">Admin</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{page}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
...@@ -10,7 +10,7 @@ type CardImageProps = { ...@@ -10,7 +10,7 @@ type CardImageProps = {
export function CardImage({ children, image, width, height }: CardImageProps) { export function CardImage({ children, image, width, height }: CardImageProps) {
return ( return (
<div className={`relative w-[${width}px] `}> <div className={`relative w-[${width}px]`}>
<Image <Image
src={image} src={image}
width={width} width={width}
......
'use client'
import Link from 'next/link' import Link from 'next/link'
import { ReactNode } from 'react' import { ReactNode } from 'react'
...@@ -9,10 +11,12 @@ type CardRootProps = { ...@@ -9,10 +11,12 @@ type CardRootProps = {
export function CardRoot({ children, link }: CardRootProps) { export function CardRoot({ children, link }: CardRootProps) {
return ( return (
<Link <Link
className="lowercase transform scale-95 hover:scale-100 transition-transform duration-300 drop-shadow" className="lowercase transform scale-95 hover:scale-100 transition-transform duration-300 drop-shadow "
href={link} href={link}
> >
<div className={`border border-gray-100 pl-0 rounded-lg max-w-[273px] `}> <div
className={`border border-gray-100 pl-0 rounded-lg max-w-[273px] h-full`}
>
{children} {children}
</div> </div>
</Link> </Link>
......
'use client'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { CoursesCard } from '@/utils/courses-array' import { getCourses } from '@/api/courses'
import { useQuery } from '@tanstack/react-query'
import { Card } from './card' import { Card } from './card'
import { import {
Carousel, Carousel,
...@@ -11,32 +14,47 @@ import { ...@@ -11,32 +14,47 @@ import {
} from './ui/carousel' } from './ui/carousel'
export function CarouselComponent() { export function CarouselComponent() {
const { data: courses } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
return ( return (
<section> <section>
<Carousel> <Carousel>
<CarouselContent> {courses ? (
{CoursesCard.map((course) => ( <CarouselContent>
<CarouselItem {courses.data.slice(0, 10).map((course) => (
key={course.id} <CarouselItem
className="sm:basis-1/2 md:basis-1/3 lg:basis-1/4 xl:basis-1/5 flex justify-center" key={course.id}
> className="sm:basis-1/2 md:basis-1/3 lg:basis-1/4 xl:basis-1/5 flex justify-center"
<Card.Root link={`curso/${course.id}`}> >
<Card.Image image={course.image.src} width={240} height={320}> <Card.Root link={`/curso/${course.id}`}>
<Card.Title title={course.title} /> <Card.Image
</Card.Image> image={course.mobileBanner || ''}
<Card.Content description={course.hours}> width={240}
<Card.Icon icon={Clock4} /> height={320}
</Card.Content> >
<Card.Content description={course.category}> <Card.Title title={course.name} />
<Card.Icon icon={User} /> </Card.Image>
</Card.Content> <Card.Content
<Card.Content description={course.calender}> description={`${String(course.workload)} horas`}
<Card.Icon icon={Calendar} /> >
</Card.Content> <Card.Icon icon={Clock4} />
</Card.Root> </Card.Content>
</CarouselItem> <Card.Content description={course?.audience?.name || ''}>
))} <Card.Icon icon={User} />
</CarouselContent> </Card.Content>
<Card.Content description={course?.category?.name || ''}>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</CarouselItem>
))}
</CarouselContent>
) : (
<p>Não há cursos disponíveis</p>
)}
<CarouselPrevious /> <CarouselPrevious />
<CarouselNext /> <CarouselNext />
</Carousel> </Carousel>
......
import { Calendar, Clock4, DollarSign, UserRound } from 'lucide-react' import { CourseIdProps } from '@/api/courses'
import { format, parseISO } from 'date-fns'
import { toZonedTime } from 'date-fns-tz'
import { Calendar, Clock4, UserRound } from 'lucide-react'
type CourseProps = {
data?: CourseIdProps
}
export function CourseDetails(course: CourseProps) {
const formatDate = (dateString: string | undefined) => {
if (!dateString) return
const date = parseISO(dateString)
const utcDate = toZonedTime(date, 'UTC')
return format(utcDate, 'dd/MM/yyyy')
}
export function CourseDetails() {
return ( return (
<ul className="flex flex-col gap-8"> <ul className="flex flex-col gap-8">
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
...@@ -11,7 +26,9 @@ export function CourseDetails() { ...@@ -11,7 +26,9 @@ export function CourseDetails() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="font-thin">Duração:</span> <span className="font-thin">Duração:</span>
<span className="text-xl">30 Hrs/Curso Rápido</span> <span className="text-xl">
{course.data?.workload.toString()} Hrs/{course.data?.category.name}
</span>
</div> </div>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
...@@ -22,7 +39,7 @@ export function CourseDetails() { ...@@ -22,7 +39,7 @@ export function CourseDetails() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="font-thin">Indicado para:</span> <span className="font-thin">Indicado para:</span>
<span className="text-xl">Estudantes</span> <span className="text-xl">{course.data?.audience.name}</span>
</div> </div>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
...@@ -33,10 +50,10 @@ export function CourseDetails() { ...@@ -33,10 +50,10 @@ export function CourseDetails() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="font-thin">Início:</span> <span className="font-thin">Início:</span>
<span className="text-xl">Imediato</span> <span className="text-xl">{formatDate(course.data?.startDate)}</span>
</div> </div>
</li> </li>
<li className="flex items-center gap-4"> {/* <li className="flex items-center gap-4">
<div className="rounded-full w-[60px] h-[60px] flex items-center justify-center bg-gradient-to-r from-green-700 to-green-50"> <div className="rounded-full w-[60px] h-[60px] flex items-center justify-center bg-gradient-to-r from-green-700 to-green-50">
<div className="rounded-full flex items-center justify-center w-[57px] h-[57px] dark:bg-gray-900 bg-gray-50"> <div className="rounded-full flex items-center justify-center w-[57px] h-[57px] dark:bg-gray-900 bg-gray-50">
<DollarSign size={35} /> <DollarSign size={35} />
...@@ -46,7 +63,7 @@ export function CourseDetails() { ...@@ -46,7 +63,7 @@ export function CourseDetails() {
<span className="font-thin">Investimento:</span> <span className="font-thin">Investimento:</span>
<span className="text-xl">R$200,00</span> <span className="text-xl">R$200,00</span>
</div> </div>
</li> </li> */}
</ul> </ul>
) )
} }
import { forwardRef } from 'react'
interface InputFileProps extends React.InputHTMLAttributes<HTMLInputElement> {
id: string
values?: string
label: string
}
const InputFile = forwardRef<HTMLInputElement, InputFileProps>(
({ id, values, label, ...inputProps }, ref) => {
return (
<label
htmlFor={id}
className="flex justify-center items-center border border-dotted rounded-sm border-green-400 bg-green-400/10 cursor-pointer hover:bg-green-400/5 text-green-400 text-sm h-10 mt-6"
>
{values || label}
<input
id={id}
type="file"
accept="image/png, image/jpeg, image/jpg"
className="hidden"
ref={ref}
{...inputProps}
/>
</label>
)
},
)
InputFile.displayName = 'InputFile'
export default InputFile
import { cn } from '@/lib/utils'
type SpinProps = {
className?: React.HTMLProps<HTMLElement>['className']
}
export function LoadingSpinIcon({ className }: SpinProps) {
return (
<svg
className={cn('animate-spin -ml-1 mr-3 h-5 w-5 text-white', className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)
}
import {
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog'
import { Button } from './ui/button'
type AlertDialogProps = {
title: string
description: string
handleClick: () => void
}
export default function ModalDialog({
title,
description,
handleClick,
}: AlertDialogProps) {
return (
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<Button
onClick={handleClick}
variant="outline"
className="text-gray-50 bg-red-500 hover:bg-red-500/80"
>
Continue
</Button>
</AlertDialogFooter>
</AlertDialogContent>
)
}
'use client'
import { TextField } from '@mui/material'
import { StyledInputs } from './mui/styled-inputs'
import {
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog'
import { Button } from './ui/button'
type AlertDialogProps = {
title: string
description: string
value?: string
handleClick: () => void
handleClickDelete?: () => void
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
export default function ModalInputs({
title,
description,
value,
handleClick,
handleClickDelete,
onChange,
}: AlertDialogProps) {
return (
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<TextField
label="Nome"
type="text"
value={value}
onChange={onChange}
variant="standard"
className="flex"
sx={StyledInputs({ color: '#26AAA7' })}
/>
<AlertDialogFooter className="mt-4 w-full flex !justify-between">
{handleClickDelete && (
<Button
onClick={handleClickDelete}
variant="outline"
className="text-gray-50 bg-red-500 hover:bg-red-500/80"
>
Deletar
</Button>
)}
<div className='flex gap-4'>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<Button
onClick={handleClick}
variant="outline"
className="text-gray-50 bg-green-500 hover:bg-green-500/80"
>
Salvar
</Button>
</div>
</AlertDialogFooter>
</AlertDialogContent>
)
}
...@@ -3,28 +3,36 @@ ...@@ -3,28 +3,36 @@
import { useThemeClient } from '@/hooks/useThemeClient' import { useThemeClient } from '@/hooks/useThemeClient'
import { TextField, TextFieldProps } from '@mui/material' import { TextField, TextFieldProps } from '@mui/material'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { FieldValues, Path, UseFormRegister } from 'react-hook-form'
type InputMuiProps = TextFieldProps & { type InputMuiProps<T extends FieldValues> = TextFieldProps & {
label: string label: string
variant: 'standard' | 'filled' | 'outlined' variant: 'standard' | 'filled' | 'outlined'
themeColor?: string
children?: ReactNode children?: ReactNode
name: Path<T>
register: UseFormRegister<T>
} }
export function InputMui({ export const InputMui = <T extends FieldValues>({
label, label,
variant, variant,
children, children,
...props themeColor = '#fafafa',
}: InputMuiProps) { register,
name,
...rest
}: InputMuiProps<T>) => {
const themeConfig = useThemeClient() const themeConfig = useThemeClient()
const color = themeConfig === 'dark' ? '#fafafa' : '#3C3C3C' const color = themeConfig === 'dark' ? themeColor : '#3C3C3C'
return ( return (
<TextField <TextField
label={label} label={label}
variant={variant} variant={variant}
{...props} {...register(name)}
{...rest}
sx={{ sx={{
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
'& fieldset': { '& fieldset': {
...@@ -44,10 +52,10 @@ export function InputMui({ ...@@ -44,10 +52,10 @@ export function InputMui({
color: `${color}`, color: `${color}`,
}, },
'& input': { '& input': {
color: `${color}`, color: `#fafafa`,
}, },
'& .MuiInputBase-input': { '& .MuiInputBase-input': {
color: `${color}`, color: `#fafafa`,
}, },
'& .MuiInput-underline:before': { '& .MuiInput-underline:before': {
borderBottomColor: color, borderBottomColor: color,
......
'use client'
import { getAreas } from '@/api/areas'
import { useQuery } from '@tanstack/react-query'
import { NavLinkSearchParams } from './nav-link-search-params' import { NavLinkSearchParams } from './nav-link-search-params'
export function NavLinkCategory() { export function NavLinkCategory() {
const { data: areas } = useQuery({ queryKey: ['areas'], queryFn: getAreas })
return ( return (
<nav className="container my-10"> <nav className="container my-10">
<ul className="flex flex-wrap justify-center items-center gap-8"> <ul className="flex flex-wrap justify-center items-center gap-8">
<li> {areas?.data.map((area) => (
<NavLinkSearchParams <li key={area.id}>
variant="default" <NavLinkSearchParams
href="tecnologia" variant="default"
className="uppercase" href={area.name.toLowerCase()}
> className="uppercase"
Tecnologia >
</NavLinkSearchParams> {area.name}
</li> </NavLinkSearchParams>
<li> </li>
<NavLinkSearchParams ))}
variant="default"
href="negocios"
className="uppercase"
>
Negócios
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="saude"
className="uppercase"
>
Saúde
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="educacao"
className="uppercase"
>
Educação
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="comunicacao"
className="uppercase"
>
Comunicação
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="teologia"
className="uppercase"
>
Teologia
</NavLinkSearchParams>
</li>
</ul> </ul>
</nav> </nav>
) )
......
This diff is collapsed.
'use client'
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { InputMui } from './mui/inputs' import { InputMui } from './mui/inputs'
import { Label } from './ui/label' import { Label } from './ui/label'
import { import {
...@@ -11,6 +14,8 @@ import { ...@@ -11,6 +14,8 @@ import {
} from './ui/select' } from './ui/select'
export default function SearchFilter() { export default function SearchFilter() {
const { register } = useForm()
return ( return (
<div className="container grid md:grid-cols-2 gap-8 items-end mb-8"> <div className="container grid md:grid-cols-2 gap-8 items-end mb-8">
<div className="flex items-center max-w-[712px] flex-1"> <div className="flex items-center max-w-[712px] flex-1">
...@@ -19,6 +24,8 @@ export default function SearchFilter() { ...@@ -19,6 +24,8 @@ export default function SearchFilter() {
variant="standard" variant="standard"
type="text" type="text"
className="w-full #fafafa" className="w-full #fafafa"
name="learned-today"
register={register}
/> />
<Search size={24} className="-ml-6" /> <Search size={24} className="-ml-6" />
</div> </div>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import { S3Client } from '@aws-sdk/client-s3'
export const s3 = new S3Client({
region: 'auto',
endpoint: process.env.NEXT_PUBLIC_CLOUDFLARE_ACCESS_ENDPOINT,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_CLOUDFLARE_ACCESS_KEY_ID!,
secretAccessKey: process.env.NEXT_PUBLIC_CLOUDFLARE_SECRET_ACCESS_KEY!,
},
})
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment