Formulario de Categorias
- https://ui.shadcn.com/docs/components/combobox
- Instalar popover
npx shadcn-ui@latest add popover
y commandnpx shadcn-ui@latest add command
- Crear
components/ui/combobox.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"use client"
import * as React from "react"
import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface ComboboxProps {
options: { label: string; value: any }[];
value?: any;
onChange: (value: any) => void;
};
export const Combobox = ({
options,
value,
onChange
}: ComboboxProps) => {
const [open, setOpen] = React.useState(false)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{value
? options.find((option) => option.value === value)?.label
: "Seleccione una opción..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="Buscar opciones..." />
<CommandEmpty>No se encontraron opciones.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => {
onChange(option.value)
setOpen(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
)
}
- Crear
app/(dashboard)/(routes)/teacher/courses/[uuid]/_components/CategoriesForm.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"use client";
import * as z from "zod";
import axios from "axios";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Pencil } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import { tbl_categorias, tbl_cursos } from "@prisma/client";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Combobox } from "@/components/ui/combobox";
interface CategoryFormProps {
initialData: tbl_cursos & { categoria: tbl_categorias | null} ;
id_curso: number;
options: { label: string; value: number }[];
};
const formSchema = z.object({
id_categoria: z.number().nullable(),
});
export const CategoriesForm = ({
initialData,
id_curso,
options,
}: CategoryFormProps) => {
const [isEditing, setIsEditing] = useState(false);
const toggleEdit = () => {
setIsEditing((current) => !current);
if (!isEditing) {
form.setValue("id_categoria", initialData?.id_categoria || null);
}
};
console.log(initialData)
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id_categoria: initialData?.id_categoria || null,
},
});
const { isSubmitting, isValid } = form.formState;
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await axios.patch(`/api/courses/${id_curso}`, values);
toast.success("Curso Actualizado");
toggleEdit();
router.refresh();
} catch {
toast.error("Sucedió un error al actualizar el curso");
}
}
return (
<div className="mt-6 border bg-[#cfcfcf] dark:bg-[#1f1f1f] rounded-md p-4">
<div className="font-medium flex items-center justify-between">
Categoria del curso
<Button onClick={toggleEdit} variant="customghost">
{isEditing ? (
<>Cancelar</>
) : (
<>
<Pencil className="h-4 w-4 mr-2" />
Editar categoria
</>
)}
</Button>
</div>
{!isEditing && (
<p className={cn(
"text-sm",
"text-slate-700 dark:text-white",
"mt-2",
!initialData.id_categoria && "italic"
)}>
{initialData.categoria?.nombre || "* Sin categoria *"}
</p>
)}
{isEditing && (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4 mt-4"
>
<FormField
control={form.control}
name="id_categoria"
render={({ field }) => (
<FormItem>
<FormControl>
<Combobox
options={...options}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center gap-x-2">
<Button
disabled={!isValid || isSubmitting}
type="submit"
variant="success"
>
Guardar
</Button>
</div>
</form>
</Form>
)}
</div>
)
}
- Editar
app/(dashboard)/(routes)/teacher/courses/[uuid]/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { auth } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { CircleDollarSign, LayoutDashboard } from "lucide-react";
import { TitleForm } from "./_components/TitleForm";
import { DescriptionForm } from "./_components/DescriptionForm";
import { ImageForm } from "./_components/ImageForm";
import { CategoriesForm } from "./_components/CategoriesForm";
import { Prisma } from "@prisma/client";
const CourseUuidPage = async ({
params
}: {
params: { uuid: string }
}) => {
const { userId } = auth();
if (!userId) {
return redirect("/");
}
const course = await db.tbl_cursos.findFirst({
where: {
uuid: params.uuid,
id_usuario: userId
},
include: {
categoria: true
},
});
console.log(course);
const categories = await db.tbl_categorias.findMany({
orderBy: {
nombre: "asc"
}
});
if (!course) {
return redirect("/");
}
const requiredFields = [
course.titulo,
course.descripcion,
course.imagen_url,
course.precio,
course.id_categoria];
const totalFields = requiredFields.length;
const completedFields = requiredFields.filter(Boolean).length;
const completionText = `(${completedFields}/${totalFields})`;
return (
<div className="p-6">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-y-2">
<h1 className="text-2xl font-medium">
Configuracion del curso
</h1>
<span className="text-sm text-slate-700 dark:text-white">
Completar todos los campos {completionText}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-16">
<div>
<div className="flex items-center gap-x-2">
<div className="rounded-full flex items-center justify-center bg-sky-100 dark:bg-[#1f1f1f] p-2">
<LayoutDashboard className="h-8 w-8 text-teal-700 dark:text-yellow-500" />
</div>
<h2 className="text-xl">
Personaliza tu curso
</h2>
</div>
<TitleForm
initialData={course}
id_curso={course.id_curso}
/>
<DescriptionForm
initialData={course}
id_curso={course.id_curso}
/>
<CategoriesForm
initialData={course}
id_curso={course.id_curso}
options={categories.map((category) => ({
label: category.nombre,
value: category.id_categoria,
}))}
/>
<ImageForm
initialData={course}
id_curso={course.id_curso}
/>
</div>
</div>
</div>
);
}
export default CourseUuidPage;
Formulario de Precio
- Crear
lib/format.ts
1
2
3
4
5
6
7
export const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
}).format(price)
}
- Crear
app/(dashboard)/(routes)/teacher/courses/[uuid]/_components/PriceForm.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"use client";
import * as z from "zod";
import axios from "axios";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Pencil } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import { tbl_cursos } from "@prisma/client";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { formatPrice } from "@/lib/format";
interface PriceFormProps {
initialData: tbl_cursos;
id_curso: number;
};
const formSchema = z.object({
precio: z.coerce.number().positive().min(0.00, {
message: "El precio es requerido",
})
});
export const PriceForm = ({
initialData,
id_curso
}: PriceFormProps) => {
const [isEditing, setIsEditing] = useState(false);
const toggleEdit = () => {
setIsEditing((current) => !current);
if (!isEditing) {
//form.reset();
form.setValue("precio", initialData?.precio || 0.00);
}
};
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
precio: initialData?.precio || undefined,
},
});
const { isSubmitting, isValid } = form.formState;
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await axios.patch(`/api/courses/${id_curso}`, values);
toast.success("Curso Actualizado");
toggleEdit();
router.refresh();
} catch {
toast.error("Sucedio un error al actualizar el curso");
}
}
return (
<div className="mt-6 border bg-[#cfcfcf] dark:bg-[#1f1f1f] rounded-md p-4">
<div className="font-medium flex items-center justify-between">
Precio del curso
<Button onClick={toggleEdit} variant="customghost">
{isEditing ? (
<>Cancelar</>
) : (
<>
<Pencil className="h-4 w-4 mr-2" />
Editar precio
</>
)}
</Button>
</div>
{!isEditing && (
<p className={cn(
"text-sm",
"text-slate-700 dark:text-white",
"mt-2",
!initialData.precio && "italic"
)}>
{initialData.precio ? formatPrice(initialData.precio) : "* Sin precio *"}
</p>
)}
{isEditing && (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4 mt-4"
>
<FormField
control={form.control}
name="precio"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="number"
min="0"
step="0.01"
disabled={isSubmitting}
placeholder="ej. $99.99"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center gap-x-2">
<Button
disabled={!isValid || isSubmitting}
type="submit"
variant="success"
>
Guardar
</Button>
</div>
</form>
</Form>
)}
</div>
)
}
- Editar
app/(dashboard)/(routes)/teacher/courses/[uuid]/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { auth } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { CircleDollarSign, LayoutDashboard } from "lucide-react";
import { TitleForm } from "./_components/TitleForm";
import { DescriptionForm } from "./_components/DescriptionForm";
import { ImageForm } from "./_components/ImageForm";
import { CategoriesForm } from "./_components/CategoriesForm";
import { PriceForm } from "./_components/PriceForm";
const CourseUuidPage = async ({
params
}: {
params: { uuid: string }
}) => {
const { userId } = auth();
if (!userId) {
return redirect("/");
}
const course = await db.tbl_cursos.findFirst({
where: {
uuid: params.uuid,
id_usuario: userId
}
});
const categories = await db.tbl_categorias.findMany({
orderBy: {
nombre: "asc"
}
});
if (course?.id_categoria !== null) {
let category = await db.tbl_categorias.findUnique({
where: {
id_categoria: course?.id_categoria
}
});
var categoryName = category?.nombre;
}
if (!course) {
return redirect("/");
}
const requiredFields = [
course.titulo,
course.descripcion,
course.imagen_url,
course.precio,
course.id_categoria];
const totalFields = requiredFields.length;
const completedFields = requiredFields.filter(Boolean).length;
const completionText = `(${completedFields}/${totalFields})`;
return (
<div className="p-6">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-y-2">
<h1 className="text-2xl font-medium">
Configuracion del curso
</h1>
<span className="text-sm text-slate-700 dark:text-white">
Completar todos los campos {completionText}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-16">
<div>
<div className="flex items-center gap-x-2">
<div className="rounded-full flex items-center justify-center bg-sky-100 dark:bg-[#1f1f1f] p-2">
<LayoutDashboard className="h-8 w-8 text-teal-700 dark:text-yellow-500" />
</div>
<h2 className="text-xl">
Personaliza tu curso
</h2>
</div>
<TitleForm
initialData={course}
id_curso={course.id_curso}
/>
<DescriptionForm
initialData={course}
id_curso={course.id_curso}
/>
<CategoriesForm
initialData={course}
id_curso={course.id_curso}
options={categories.map((category) => ({
label: category.nombre,
value: category.id_categoria,
}))}
category={categoryName}
/>
<ImageForm
initialData={course}
id_curso={course.id_curso}
/>
</div>
<div className="space-y-6">
<div className="flex items-center gap-x-2">
<div className="rounded-full flex items-center justify-center bg-sky-100 dark:bg-[#1f1f1f] p-2">
<CircleDollarSign className="h-8 w-8 text-teal-700 dark:text-yellow-500" />
</div>
<h2 className="text-xl">
Pon precio a tu curso
</h2>
</div>
<PriceForm
initialData={course}
id_curso={course.id_curso}
/>
</div>
</div>
</div>
);
}
export default CourseUuidPage;