remove client frm id.saladeaula.digital
This commit is contained in:
13
id.saladeaula.digital/client/.gitignore
vendored
13
id.saladeaula.digital/client/.gitignore
vendored
@@ -1,13 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
/node_modules/
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# React Router
|
|
||||||
/.react-router/
|
|
||||||
/build/
|
|
||||||
|
|
||||||
# Cloudflare
|
|
||||||
.mf
|
|
||||||
.wrangler
|
|
||||||
.dev.vars*
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "none",
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# [id.saladeaula.digital](https://id.saladeaula.digital)
|
|
||||||
|
|
||||||
O código-fonte para [id.saladeaula.digital](https://id.saladeaula.digital), construído com [React Router](https://github.com/remix-run/react-router).
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
@import 'tailwindcss' source('.');
|
|
||||||
@import 'tw-animate-css';
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--font-sans:
|
|
||||||
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
||||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--radius: 0.65rem;
|
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.145 0 0);
|
|
||||||
--popover: oklch(1 0 0);
|
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--secondary: oklch(0.97 0 0);
|
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
|
||||||
--accent: oklch(0.97 0 0);
|
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
|
||||||
--input: oklch(0.922 0 0);
|
|
||||||
--ring: oklch(0.708 0 0);
|
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0 0 0);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.205 0 0);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.205 0 0);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.269 0 0);
|
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M16.2756 23.4353L8.93847 20.1298C8.7383 20.0015 8.48167 20.0015 8.27893 20.1298L0.941837 23.4353C0.533793 23.6945 0 23.4019 0 22.9194V1.12629C0.00256631 0.787535 0.277162 0.512939 0.615915 0.512939H16.6066C16.9454 0.512939 17.22 0.787535 17.22 1.12629V22.9194C17.22 23.4019 16.6862 23.6945 16.2781 23.4353H16.2756Z" fill="#8CD366"></path>
|
|
||||||
<path d="M10.7274 3.71313H3.34668V6.41803H10.7274V3.71313Z" fill="#2E3524"></path>
|
|
||||||
<path d="M9.42115 8.4939H3.34668V10.6496H9.42115V8.4939Z" fill="#2E3524"></path>
|
|
||||||
<path d="M10.7274 12.7263H3.34668V15.4312H10.7274V12.7263Z" fill="#2E3524"></path>
|
|
||||||
<path d="M12.9984 13.6731H12.9958C12.5111 13.6731 12.1182 14.066 12.1182 14.5508V14.5533C12.1182 15.0381 12.5111 15.431 12.9958 15.431H12.9984C13.4831 15.431 13.8761 15.0381 13.8761 14.5533V14.5508C13.8761 14.066 13.4831 13.6731 12.9984 13.6731Z" fill="#2E3524"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 987 B |
@@ -1,59 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Checkbox({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
data-slot="checkbox"
|
|
||||||
className={cn(
|
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
data-slot="checkbox-indicator"
|
|
||||||
className="flex items-center justify-center text-current transition-none"
|
|
||||||
>
|
|
||||||
<CheckIcon className="size-3.5" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import {
|
|
||||||
Controller,
|
|
||||||
FormProvider,
|
|
||||||
useFormContext,
|
|
||||||
useFormState,
|
|
||||||
type ControllerProps,
|
|
||||||
type FieldPath,
|
|
||||||
type FieldValues,
|
|
||||||
} from "react-hook-form"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
|
|
||||||
const Form = FormProvider
|
|
||||||
|
|
||||||
type FormFieldContextValue<
|
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
||||||
> = {
|
|
||||||
name: TName
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
||||||
{} as FormFieldContextValue
|
|
||||||
)
|
|
||||||
|
|
||||||
const FormField = <
|
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
||||||
>({
|
|
||||||
...props
|
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
|
||||||
return (
|
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
||||||
<Controller {...props} />
|
|
||||||
</FormFieldContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFormField = () => {
|
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
|
||||||
const itemContext = React.useContext(FormItemContext)
|
|
||||||
const { getFieldState } = useFormContext()
|
|
||||||
const formState = useFormState({ name: fieldContext.name })
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
|
||||||
|
|
||||||
if (!fieldContext) {
|
|
||||||
throw new Error("useFormField should be used within <FormField>")
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = itemContext
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: fieldContext.name,
|
|
||||||
formItemId: `${id}-form-item`,
|
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
|
||||||
formMessageId: `${id}-form-item-message`,
|
|
||||||
...fieldState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormItemContextValue = {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
||||||
{} as FormItemContextValue
|
|
||||||
)
|
|
||||||
|
|
||||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
const id = React.useId()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItemContext.Provider value={{ id }}>
|
|
||||||
<div
|
|
||||||
data-slot="form-item"
|
|
||||||
className={cn("grid gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</FormItemContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
const { error, formItemId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="form-label"
|
|
||||||
data-error={!!error}
|
|
||||||
className={cn("data-[error=true]:text-destructive", className)}
|
|
||||||
htmlFor={formItemId}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slot
|
|
||||||
data-slot="form-control"
|
|
||||||
id={formItemId}
|
|
||||||
aria-describedby={
|
|
||||||
!error
|
|
||||||
? `${formDescriptionId}`
|
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
|
||||||
}
|
|
||||||
aria-invalid={!!error}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
const { formDescriptionId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="form-description"
|
|
||||||
id={formDescriptionId}
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
const { error, formMessageId } = useFormField()
|
|
||||||
const body = error ? String(error?.message ?? "") : props.children
|
|
||||||
|
|
||||||
if (!body) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="form-message"
|
|
||||||
id={formMessageId}
|
|
||||||
className={cn("text-destructive text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
useFormField,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormMessage,
|
|
||||||
FormField,
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { isbot } from 'isbot'
|
|
||||||
import { renderToReadableStream } from 'react-dom/server'
|
|
||||||
import type { AppLoadContext, EntryContext } from 'react-router'
|
|
||||||
import { ServerRouter } from 'react-router'
|
|
||||||
|
|
||||||
export default async function handleRequest(
|
|
||||||
request: Request,
|
|
||||||
responseStatusCode: number,
|
|
||||||
responseHeaders: Headers,
|
|
||||||
routerContext: EntryContext,
|
|
||||||
_loadContext: AppLoadContext
|
|
||||||
) {
|
|
||||||
let shellRendered = false
|
|
||||||
const userAgent = request.headers.get('user-agent')
|
|
||||||
|
|
||||||
const body = await renderToReadableStream(
|
|
||||||
<ServerRouter context={routerContext} url={request.url} />,
|
|
||||||
{
|
|
||||||
onError(error: unknown) {
|
|
||||||
responseStatusCode = 500
|
|
||||||
// Log streaming rendering errors from inside the shell. Don't log
|
|
||||||
// errors encountered during initial shell rendering since they'll
|
|
||||||
// reject and get logged in handleDocumentRequest.
|
|
||||||
if (shellRendered) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
shellRendered = true
|
|
||||||
|
|
||||||
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
|
||||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
|
||||||
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
|
|
||||||
await body.allReady
|
|
||||||
}
|
|
||||||
|
|
||||||
responseHeaders.set('Content-Type', 'text/html')
|
|
||||||
return new Response(body, {
|
|
||||||
headers: responseHeaders,
|
|
||||||
status: responseStatusCode
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs))
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import {
|
|
||||||
isRouteErrorResponse,
|
|
||||||
Links,
|
|
||||||
Meta,
|
|
||||||
Outlet,
|
|
||||||
Scripts,
|
|
||||||
ScrollRestoration
|
|
||||||
} from 'react-router'
|
|
||||||
|
|
||||||
import type { Route } from './+types/root'
|
|
||||||
import './app.css'
|
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html lang="pt-br" className="dark">
|
|
||||||
<head>
|
|
||||||
<meta charSet="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<Meta />
|
|
||||||
<Links />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{children}
|
|
||||||
<ScrollRestoration />
|
|
||||||
<Scripts />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return <Outlet />
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
||||||
let message = 'Oops!'
|
|
||||||
let details = 'Ocorreu um erro inesperado.'
|
|
||||||
let stack: string | undefined
|
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
|
||||||
message = error.status === 404 ? '404' : 'Erro'
|
|
||||||
details =
|
|
||||||
error.status === 404
|
|
||||||
? 'A página solicitada não foi encontrada.'
|
|
||||||
: error.statusText || details
|
|
||||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
|
||||||
details = error.message
|
|
||||||
stack = error.stack
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="pt-16 p-4 container mx-auto">
|
|
||||||
<h1>{message}</h1>
|
|
||||||
<p>{details}</p>
|
|
||||||
{stack && (
|
|
||||||
<pre className="w-full p-4 overflow-x-auto">
|
|
||||||
<code>{stack}</code>
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import {
|
|
||||||
index,
|
|
||||||
layout,
|
|
||||||
route,
|
|
||||||
type RouteConfig
|
|
||||||
} from '@react-router/dev/routes'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
layout('routes/layout.tsx', [
|
|
||||||
index('routes/index.tsx'),
|
|
||||||
route('/signup', 'routes/signup.tsx'),
|
|
||||||
route('/forgot', 'routes/forgot.tsx'),
|
|
||||||
route('/deny', 'routes/deny.tsx')
|
|
||||||
]),
|
|
||||||
route('/authorize', 'routes/authorize.ts'),
|
|
||||||
route('/*', 'routes/upstream.ts')
|
|
||||||
] satisfies RouteConfig
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import type { Route } from './+types'
|
|
||||||
|
|
||||||
import { parse } from 'cookie'
|
|
||||||
|
|
||||||
export const OK = 200
|
|
||||||
export const FOUND = 302
|
|
||||||
export const INTERNAL_SERVER_ERROR = 500
|
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
|
||||||
const cookies = parse(request.headers.get('Cookie') || '')
|
|
||||||
const url = new URL(request.url)
|
|
||||||
const loginUrl = new URL('/', url.origin)
|
|
||||||
const issuerUrl = new URL('/authorize', context.cloudflare.env.ISSUER_URL)
|
|
||||||
issuerUrl.search = url.search
|
|
||||||
loginUrl.search = url.search
|
|
||||||
|
|
||||||
if (!cookies?.__session) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: FOUND,
|
|
||||||
headers: {
|
|
||||||
Location: loginUrl.toString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch(issuerUrl.toString(), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: new Headers([
|
|
||||||
['Content-Type', 'application/json'],
|
|
||||||
['Cookie', request.headers.get('Cookie') as string]
|
|
||||||
]),
|
|
||||||
redirect: 'manual'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (r.status === FOUND) {
|
|
||||||
return new Response(await r.text(), {
|
|
||||||
status: r.status,
|
|
||||||
headers: r.headers
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Authorize response', {
|
|
||||||
json: await r.json(),
|
|
||||||
headers: r.headers,
|
|
||||||
status: r.status
|
|
||||||
})
|
|
||||||
|
|
||||||
// Deny authorization if user lacks scopes requested by client
|
|
||||||
if (r.status === FOUND) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: r.status,
|
|
||||||
headers: {
|
|
||||||
Location: new URL('/deny', url.origin).toString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: FOUND,
|
|
||||||
headers: {
|
|
||||||
Location: loginUrl.toString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return new Response(null, { status: INTERNAL_SERVER_ERROR })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { LockIcon } from 'lucide-react'
|
|
||||||
import type { Route } from './+types'
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
|
||||||
return [{ title: 'Acesso negado · EDUSEG®' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Deny({}: Route.ComponentProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col text-center items-center gap-6">
|
|
||||||
<LockIcon className="size-12" />
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<h1 className="text-xl text-gray-10 font-bold">Acesso negado.</h1>
|
|
||||||
<p>Você não tem permissão.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import type { Route } from './+types'
|
|
||||||
|
|
||||||
import { Link } from 'react-router'
|
|
||||||
|
|
||||||
import logo from '@/components/logo.svg'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage
|
|
||||||
} from '@/components/ui/form'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
username: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.nonempty('Digite seu Email ou CPF')
|
|
||||||
.refine((val) => {
|
|
||||||
const onlyDigits = val.replace(/\D/g, '')
|
|
||||||
|
|
||||||
return onlyDigits.length === 11
|
|
||||||
? isValidCPF(val)
|
|
||||||
: z.email().safeParse(val).success
|
|
||||||
}, 'Deve ser um Email ou CPF válido')
|
|
||||||
})
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
|
||||||
return [{ title: 'Redefinir senha · EDUSEG®' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Forgot({}: Route.ComponentProps) {
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(schema)
|
|
||||||
})
|
|
||||||
const { control, handleSubmit, formState } = form
|
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
|
||||||
console.log(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-full max-w-xs grid gap-6">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="border border-white/15 bg-white/5 px-2.5 py-3 rounded-xl">
|
|
||||||
<img src={logo} alt="EDUSEG®" className="block size-12" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center space-y-1.5">
|
|
||||||
<h1 className="text-2xl font-semibold font-display text-balance">
|
|
||||||
Redefinir senha
|
|
||||||
</h1>
|
|
||||||
<p className="text-white/50 text-sm">
|
|
||||||
Digite seu email e lhe enviaremos um email com as instruções para
|
|
||||||
redefinir sua senha.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="username"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email ou CPF</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
autoFocus={true}
|
|
||||||
placeholder="seu@email.com"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-lime-400 cursor-pointer"
|
|
||||||
disabled={formState.isSubmitting}
|
|
||||||
>
|
|
||||||
Enviar instruções
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<p className="text-white/50 text-xs text-center">
|
|
||||||
Lembrou da senha?{' '}
|
|
||||||
<Link to="/" className="underline hover:no-underline">
|
|
||||||
Faça login
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import type { Route } from './+types'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage
|
|
||||||
} from '@/components/ui/form'
|
|
||||||
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { Loader2Icon } from 'lucide-react'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { Link, useFetcher } from 'react-router'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import logo from '@/components/logo.svg'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { FOUND, INTERNAL_SERVER_ERROR, OK } from './authorize'
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
username: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.nonempty('Digite seu Email ou CPF')
|
|
||||||
.refine((val) => {
|
|
||||||
const onlyDigits = val.replace(/\D/g, '')
|
|
||||||
|
|
||||||
return onlyDigits.length === 11
|
|
||||||
? isValidCPF(val)
|
|
||||||
: z.email().safeParse(val).success
|
|
||||||
}, 'Deve ser um Email ou CPF válido'),
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.nonempty('Digite sua senha')
|
|
||||||
.min(6, 'Deve ter no mínimo 6 caracteres')
|
|
||||||
})
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
|
||||||
return [{ title: 'EDUSEG®' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function action({ request, context }: Route.ActionArgs) {
|
|
||||||
const issuerUrl = new URL('/session', context.cloudflare.env.ISSUER_URL)
|
|
||||||
const formData = Object.fromEntries(await request.formData())
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch(issuerUrl.toString(), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (r.status !== OK) {
|
|
||||||
return Response.json(await r.json(), {
|
|
||||||
status: r.status,
|
|
||||||
headers: r.headers
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(request.url)
|
|
||||||
url.pathname = '/authorize'
|
|
||||||
|
|
||||||
const headers = new Headers(r.headers)
|
|
||||||
headers.set('Location', url.toString())
|
|
||||||
|
|
||||||
return new Response(await r.text(), {
|
|
||||||
status: FOUND,
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return Response.json({}, { status: INTERNAL_SERVER_ERROR })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Index({}: Route.ComponentProps) {
|
|
||||||
const [show, setShow] = useState(false)
|
|
||||||
const fetcher = useFetcher()
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(schema)
|
|
||||||
})
|
|
||||||
const { control, handleSubmit, formState, setError } = form
|
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
|
||||||
await fetcher.submit(data, { method: 'post' })
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fetcher.state === 'idle' && fetcher.data) {
|
|
||||||
const message = fetcher.data?.message
|
|
||||||
|
|
||||||
switch (message) {
|
|
||||||
case 'User not found':
|
|
||||||
return setError('username', {
|
|
||||||
message:
|
|
||||||
'Conta não encontrada. Certifique-se de que está usando o Email ou CPF correto.',
|
|
||||||
type: 'manual'
|
|
||||||
})
|
|
||||||
case 'Invalid credentials':
|
|
||||||
return setError('password', {
|
|
||||||
message: 'A senha está incorreta.',
|
|
||||||
type: 'manual'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [fetcher.state, fetcher.data])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-full max-w-xs grid gap-6">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="border border-white/15 bg-white/5 px-2.5 py-3 rounded-xl">
|
|
||||||
<img src={logo} alt="EDUSEG®" className="block size-12" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
|
|
||||||
<div className="text-center space-y-1.5">
|
|
||||||
<h1 className="text-2xl font-semibold font-display text-balance">
|
|
||||||
Entrar
|
|
||||||
</h1>
|
|
||||||
<p className="text-white/50 text-sm">
|
|
||||||
Não tem uma senha?{' '}
|
|
||||||
<Link
|
|
||||||
to="/signup"
|
|
||||||
className="font-medium text-white hover:underline"
|
|
||||||
>
|
|
||||||
Criar senha
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="username"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email ou CPF</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
autoFocus={true}
|
|
||||||
placeholder="seu@email.com"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="password"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex">
|
|
||||||
<FormLabel>Senha</FormLabel>
|
|
||||||
<Link
|
|
||||||
to="/forgot"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Esqueceu sua senha?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type={show ? 'text' : 'password'}
|
|
||||||
placeholder="••••••••"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Checkbox
|
|
||||||
id="showPassword"
|
|
||||||
onClick={() => setShow((x) => !x)}
|
|
||||||
tabIndex={-1}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="showPassword">Mostrar senha</Label>
|
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-lime-400 cursor-pointer"
|
|
||||||
disabled={formState.isSubmitting}
|
|
||||||
>
|
|
||||||
{formState.isSubmitting && (
|
|
||||||
<Loader2Icon className="animate-spin" />
|
|
||||||
)}
|
|
||||||
Entrar
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<p className="text-white/50 text-xs text-center">
|
|
||||||
Ao fazer login, você concorda com nossa{' '}
|
|
||||||
<a
|
|
||||||
href="//eduseg.com.br/politica"
|
|
||||||
target="_blank"
|
|
||||||
className="underline hover:no-underline"
|
|
||||||
>
|
|
||||||
política de privacidade
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { ChevronLeftIcon } from 'lucide-react'
|
|
||||||
import { Outlet } from 'react-router'
|
|
||||||
|
|
||||||
export default function Layout() {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10 relative">
|
|
||||||
<a
|
|
||||||
href="//eduseg.com.br"
|
|
||||||
className="flex items-center gap-0.5 absolute top-5 left-5 text-sm z-1"
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="size-5" /> Página inicial
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div className="w-full max-w-sm relative z-1">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full top-1/2 max-w-sm absolute pt-12 -translate-y-1/2">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-0 grid grid-cols-2 opacity-20"
|
|
||||||
>
|
|
||||||
<div className="blur-[106px] h-56 bg-gradient-to-br to-lime-400 from-lime-700"></div>
|
|
||||||
<div className="blur-[106px] h-42 bg-gradient-to-r from-lime-400 to-lime-600"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import type { Route } from './+types'
|
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Link } from 'react-router'
|
|
||||||
|
|
||||||
import logo from '@/components/logo.svg'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage
|
|
||||||
} from '@/components/ui/form'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
name: z.string().trim().nonempty('Digite seu nome'),
|
|
||||||
email: z.email('Digite seu email'),
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.nonempty('Digite sua senha')
|
|
||||||
.min(6, 'Deve ter no mínimo 6 caracteres'),
|
|
||||||
cpf: z
|
|
||||||
.string()
|
|
||||||
.nonempty('Digite seu CPF')
|
|
||||||
.refine(isValidCPF, 'Deve ser um CPF válido')
|
|
||||||
})
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
|
||||||
return [{ title: 'Criar conta · EDUSEG®' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Signup({}: Route.ComponentProps) {
|
|
||||||
const [show, setShow] = useState(false)
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(schema)
|
|
||||||
})
|
|
||||||
const { control, handleSubmit, formState } = form
|
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
|
||||||
console.log(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-full max-w-xs grid gap-6">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="border border-white/15 bg-white/5 px-2.5 py-3 rounded-xl">
|
|
||||||
<img src={logo} alt="EDUSEG®" className="block size-12" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center space-y-1.5">
|
|
||||||
<h1 className="text-2xl font-semibold font-display text-balance">
|
|
||||||
Criar conta
|
|
||||||
</h1>
|
|
||||||
<p className="text-white/50 text-sm">
|
|
||||||
Já tem uma conta?{' '}
|
|
||||||
<Link to="/" className="font-medium text-white hover:underline">
|
|
||||||
Faça login
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Nome</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input autoFocus={true} placeholder="Seu nome" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="seu@email.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="cpf"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>CPF</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="___.___.___-__" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="password"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Senha</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type={show ? 'text' : 'password'}
|
|
||||||
placeholder="••••••••"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Checkbox
|
|
||||||
id="showPassword"
|
|
||||||
onClick={() => setShow((x) => !x)}
|
|
||||||
tabIndex={-1}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="showPassword">Mostrar senha</Label>
|
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-lime-400 cursor-pointer"
|
|
||||||
disabled={formState.isSubmitting}
|
|
||||||
>
|
|
||||||
Criar conta
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<p className="text-white/50 text-xs text-center">
|
|
||||||
Ao fazer login, você concorda com nossa{' '}
|
|
||||||
<a
|
|
||||||
href="//eduseg.com.br/politica"
|
|
||||||
target="_blank"
|
|
||||||
className="underline hover:no-underline"
|
|
||||||
>
|
|
||||||
política de privacidade
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { Route } from './+types'
|
|
||||||
|
|
||||||
export const loader = proxy
|
|
||||||
export const action = proxy
|
|
||||||
|
|
||||||
async function proxy({
|
|
||||||
request,
|
|
||||||
context
|
|
||||||
}: Route.ActionArgs): Promise<Response> {
|
|
||||||
const pathname = new URL(request.url).pathname
|
|
||||||
const url = new URL(pathname, context.cloudflare.env.ISSUER_URL)
|
|
||||||
const headers = new Headers(request.headers)
|
|
||||||
const response = await fetch(url.toString(), {
|
|
||||||
method: request.method,
|
|
||||||
headers,
|
|
||||||
...(['GET', 'HEAD'].includes(request.method)
|
|
||||||
? {}
|
|
||||||
: { body: await request.text() })
|
|
||||||
})
|
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type') || ''
|
|
||||||
const body =
|
|
||||||
contentType.includes('application/json') || contentType.startsWith('text/')
|
|
||||||
? await response.text()
|
|
||||||
: await response.arrayBuffer()
|
|
||||||
|
|
||||||
return new Response(body, {
|
|
||||||
status: response.status,
|
|
||||||
headers: response.headers
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "app/app.css",
|
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"components": "@/components",
|
|
||||||
"utils": "@/lib/utils",
|
|
||||||
"ui": "@/components/ui",
|
|
||||||
"lib": "@/lib",
|
|
||||||
"hooks": "@/hooks"
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide"
|
|
||||||
}
|
|
||||||
5337
id.saladeaula.digital/client/package-lock.json
generated
5337
id.saladeaula.digital/client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "id-saladeaula-digital",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "react-router build",
|
|
||||||
"cf-typegen": "wrangler types",
|
|
||||||
"deploy": "npm run build && wrangler deploy",
|
|
||||||
"dev": "react-router dev",
|
|
||||||
"postinstall": "npm run cf-typegen",
|
|
||||||
"preview": "npm run build && vite preview",
|
|
||||||
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@brazilian-utils/brazilian-utils": "^1.0.0-rc.12",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@react-router/dev": "^7.9.5",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cookie": "^1.0.2",
|
|
||||||
"isbot": "^5.1.31",
|
|
||||||
"lucide-react": "^0.548.0",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react-hook-form": "^7.65.0",
|
|
||||||
"react-router": "^7.9.5",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"zod": "^4.1.12"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@cloudflare/vite-plugin": "^1.13.17",
|
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
|
||||||
"@types/node": "^24",
|
|
||||||
"@types/react": "^19.2.2",
|
|
||||||
"@types/react-dom": "^19.2.2",
|
|
||||||
"@types/statuses": "^2.0.6",
|
|
||||||
"tailwindcss": "^4.1.16",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vite": "^7.1.12",
|
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
|
||||||
"wrangler": "^4.45.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="0.54126" width="40" height="40" rx="8" fill="#2E3524"/>
|
|
||||||
<path d="M30.1297 34.3155L21.0128 30.2083C20.7628 30.0489 20.4441 30.0489 20.1941 30.2083L11.0773 34.3155C10.5705 34.6388 9.90771 34.2754 9.90771 33.6745V6.59115C9.90771 6.17148 10.2483 5.83093 10.6679 5.83093H30.539C30.9587 5.83093 31.2992 6.17148 31.2992 6.59115V33.6745C31.2992 34.2754 30.6353 34.6388 30.1297 34.3155Z" fill="#8CD366"/>
|
|
||||||
<path d="M22.3944 15.2321H13.4438V17.9107H22.3944V15.2321Z" fill="#2E3524"/>
|
|
||||||
<path d="M24.1843 20.1695H13.4438V23.731H24.1843V20.1695Z" fill="#2E3524"/>
|
|
||||||
<path d="M24.1843 9.41989H13.4438V12.9813H24.1843V9.41989Z" fill="#2E3524"/>
|
|
||||||
<path d="M27.7643 22.836C27.7643 22.3418 27.3636 21.9411 26.8693 21.9411C26.375 21.9411 25.9744 22.3418 25.9744 22.836C25.9744 23.3303 26.375 23.731 26.8693 23.731C27.3636 23.731 27.7643 23.3303 27.7643 22.836Z" fill="#2E3524"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 991 B |
@@ -1,8 +0,0 @@
|
|||||||
import type { Config } from "@react-router/dev/config";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
ssr: true,
|
|
||||||
future: {
|
|
||||||
unstable_viteEnvironmentApi: true,
|
|
||||||
},
|
|
||||||
} satisfies Config;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
".react-router/types/**/*",
|
|
||||||
"app/**/*",
|
|
||||||
"app/**/.server/**/*",
|
|
||||||
"app/**/.client/**/*",
|
|
||||||
"workers/**/*",
|
|
||||||
"worker-configuration.d.ts"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"strict": true,
|
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
||||||
"types": ["vite/client"],
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ES2022",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"rootDirs": [".", "./.react-router/types"],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./app/*"]
|
|
||||||
},
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.node.json" },
|
|
||||||
{ "path": "./tsconfig.cloudflare.json" }
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"checkJs": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./app/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["vite.config.ts"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"strict": true,
|
|
||||||
"types": ["node"],
|
|
||||||
"lib": ["ES2022"],
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ES2022",
|
|
||||||
"moduleResolution": "bundler"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
||||||
import { reactRouter } from '@react-router/dev/vite'
|
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
cloudflare({ viteEnvironment: { name: 'ssr' } }),
|
|
||||||
tailwindcss(),
|
|
||||||
reactRouter(),
|
|
||||||
tsconfigPaths()
|
|
||||||
]
|
|
||||||
})
|
|
||||||
8374
id.saladeaula.digital/client/worker-configuration.d.ts
vendored
8374
id.saladeaula.digital/client/worker-configuration.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
|||||||
import { createRequestHandler } from "react-router";
|
|
||||||
|
|
||||||
declare module "react-router" {
|
|
||||||
export interface AppLoadContext {
|
|
||||||
cloudflare: {
|
|
||||||
env: Env;
|
|
||||||
ctx: ExecutionContext;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestHandler = createRequestHandler(
|
|
||||||
() => import("virtual:react-router/server-build"),
|
|
||||||
import.meta.env.MODE
|
|
||||||
);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
async fetch(request, env, ctx) {
|
|
||||||
return requestHandler(request, {
|
|
||||||
cloudflare: { env, ctx },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
} satisfies ExportedHandler<Env>;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
name = "id-saladeaula-digital"
|
|
||||||
compatibility_date = "2025-04-04"
|
|
||||||
main = "./workers/app.ts"
|
|
||||||
routes = [
|
|
||||||
{ pattern = "id.saladeaula.digital", custom_domain = true }
|
|
||||||
]
|
|
||||||
|
|
||||||
[placement]
|
|
||||||
mode = "smart"
|
|
||||||
|
|
||||||
[vars]
|
|
||||||
ISSUER_URL = "https://duiolq49qn25e.cloudfront.net"
|
|
||||||
|
|
||||||
[observability.logs]
|
|
||||||
enabled = true
|
|
||||||
# invocation_logs = true
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user