started implementing replying functionality, cleaned up

This commit is contained in:
Joonas 2023-01-12 22:18:11 +02:00
parent fb6d36d429
commit 734a2405d3
5 changed files with 51 additions and 801 deletions

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ node_modules
/public/build
.env
/public/uploads
/prisma/dev.db
/prisma/dev.db
/app/styles

View File

@ -25,14 +25,14 @@ export async function action({ request }) {
multiPartformdata = await unstable_parseMultipartFormData(request, fileUploadHandler);
imageName = multiPartformdata.get("image").name;
} catch (err) {
errors.image = "Image size too big or something elase happened";
errors.image = "Image size too big";
}
if (typeof title !== "string" || title.length > 50) {
errors.title = "Title too long or not a string";
if (typeof title !== "string" || title.length > 50 || title.length < 3) {
errors.title = "Title too long or short";
};
if (typeof post !== "string" || post.length > 50) {
errors.post = "Post too long or not a string";
if (typeof post !== "string" || post.length > 50 || post.length < 3) {
errors.post = "Post too long or short";
};
if (Object.keys(errors).length) {
return json(errors, { status: 422 });
@ -69,12 +69,12 @@ export default function Index() {
<h1 className="text-center font-semibold text-2xl text-gray-800">Create a thread</h1>
<Form className="rounded flex flex-col text-center space-y-1 justify-center items-center " method="post" encType="multipart/form-data">
<label htmlFor="title-input">Title:
<input required minLength={3} type="text" name="title" placeholder="Lain" />
<textarea required minLength={3} type="text" name="title" placeholder="Lain" />
<p>{errors ? errors.title : ''}</p>
</label>
<br />
<label htmlFor="post-input">Post:
<input required minLength={3} type="text" name="post" placeholder="Iwakura" />
<textarea required minLength={3} type="text" name="post" placeholder="Iwakura" />
<p>{errors ? errors.post : ''}</p>
</label>
<br />
@ -89,7 +89,7 @@ export default function Index() {
{data.length > 0 ?
<ul className="flex flex-wrap justify-around items-center p-8">
{data.map(thread =>
<li className="shadow rounded-xl p-4 m-4 hover:shadow-xl scale-100 hover:scale-110 duration-500" key={thread.id}>
<li className="border shadow rounded-xl p-4 m-4 hover:shadow-xl scale-100 hover:scale-110 duration-500" key={thread.id}>
<Link className="space-y-4 flex flex-col" to={`threads/${thread.id}`}>
<div className="flex items-center justify-center">
<img className="w-48 rounded " src={`/uploads/${thread.imageName}`} alt="" />

View File

@ -1,5 +1,6 @@
import { redirect, unstable_createFileUploadHandler, unstable_parseMultipartFormData, json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import { useState } from "react";
import Overlay from "~/components/Overlay";
import prisma from "~/utils/db.server";
@ -9,6 +10,9 @@ export async function action({ request, params }) {
const clonedData = request.clone();
const formData = await clonedData.formData();
const post = formData.get("post");
const replying = formData.get("replying");
console.log(Object.fromEntries(formData));
const fileUploadHandler = unstable_createFileUploadHandler({
directory: './public/uploads/',
@ -16,14 +20,20 @@ export async function action({ request, params }) {
file: ({ filename }) => filename,
});
const multiPartformdata = await unstable_parseMultipartFormData(request, fileUploadHandler);
let imageName;
multiPartformdata.get("image") !== null ? imageName = multiPartformdata.get("image").name : imageName = null;
const errors = {};
if (typeof post !== "string" || post.length > 50) {
errors.post = "Post too long or not a string";
let imageName;
let multiPartformdata;
try {
multiPartformdata = await unstable_parseMultipartFormData(request, fileUploadHandler);
multiPartformdata.get("image") !== null ? imageName = multiPartformdata.get("image").name : imageName = null;
} catch (err) {
errors.image = "Image size too big or something elase happened";
}
if (typeof post !== "string" || post.length > 50 || post.length < 3) {
errors.post = "Post too long or short";
};
if (Object.keys(errors).length) {
return json(errors, { status: 422 });
}
@ -32,6 +42,7 @@ export async function action({ request, params }) {
data: {
comment: post,
imageName: imageName,
replyingTo: parseInt(replying),
postId: parseInt(threadId),
},
});
@ -56,6 +67,7 @@ export async function loader({ params }) {
export default function Thread() {
const data = useLoaderData();
const actionData = useActionData();
const [replying, setReplying] = useState();
return (
<Overlay>
@ -63,16 +75,19 @@ export default function Thread() {
<div className="">
<span><strong>{data.title}</strong> Post id: <strong>{data.id}</strong> Created at: <strong>{data.createdAt}</strong> Reply count: <strong>{data.posts.length}</strong></span>
<br />
<a href={`/uploads/${data.imageName}`} target="_blank"><img src={`/uploads/${data.imageName}`} alt="post image" width={240} /></a>
<a href={`/uploads/${data.imageName}`} target="_blank" referrerPolicy="no-referrer"><img src={`/uploads/${data.imageName}`} alt="post image" width={240} /></a>
<p>{data.post}</p>
</div>
<ul className="flex flex-col m-8 even:bg-neutral-50 odd:bg-neutral-100">
{data.posts.map(post =>
<li className="p-4" key={post.id}>
<a name={`${post.id}`}></a>
<span>Reply id: <strong>{post.id}</strong> Replied at: <strong>{post.createdAt}</strong></span>
<span>Reply id: <strong>{post.id}</strong> Replied at: <strong>{post.createdAt} </strong></span>
<button className="text-sky-600" onClick={() => {
setReplying(post.id)
}}>Reply</button>
<br />
{post.imageName !== null ? <img width={240} src={`/uploads/${post.imageName}`} alt="post image" /> : ''}
{post.imageName !== null ? <a href={`/uploads/${post.imageName}`} target="_blank" referrerPolicy="no-referrer"><img width={240} src={`/uploads/${post.imageName}`} alt="post image" /></a> : ''}
<p>{post.comment}</p>
</li>
)}
@ -80,12 +95,14 @@ export default function Thread() {
</div>
<Form className="bg-slate-100 p-4 space-y-2 shadow flex flex-col justify-center items-center" method="post" encType="multipart/form-data">
<label htmlFor="text-input">
{replying ? <p>Replying to reply id: {replying}</p> : ''}
Post: <input required minLength={3} name="post" type="text" />
{actionData ? <p>{actionData.post}</p> : ''}
</label>
<label htmlFor="image-upload">
Image: <input accept="image/*" type="file" name="image" id="image-upload" />
</label>
<input className="hidden" type="number" name="replying" value={replying} />
<button className="bg-white p-2 rounded-full" type="submit">Submit</button>
</Form>
</Overlay>

View File

@ -1,769 +0,0 @@
/*
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.m-4 {
margin: 1rem;
}
.m-8 {
margin: 2rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-12 {
margin-bottom: 3rem;
}
.flex {
display: flex;
}
.w-48 {
width: 12rem;
}
.scale-100 {
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-around {
justify-content: space-around;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: 9999px;
}
.rounded-xl {
border-radius: 0.75rem;
}
.border {
border-width: 1px;
}
.bg-slate-50 {
--tw-bg-opacity: 1;
background-color: rgb(248 250 252 / var(--tw-bg-opacity));
}
.bg-slate-100 {
--tw-bg-opacity: 1;
background-color: rgb(241 245 249 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.p-4 {
padding: 1rem;
}
.p-8 {
padding: 2rem;
}
.p-20 {
padding: 5rem;
}
.p-2 {
padding: 0.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.text-center {
text-align: center;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-semibold {
font-weight: 600;
}
.tracking-tighter {
letter-spacing: -0.05em;
}
.text-sky-600 {
--tw-text-opacity: 1;
color: rgb(2 132 199 / var(--tw-text-opacity));
}
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.duration-500 {
transition-duration: 500ms;
}
.odd\:bg-neutral-100:nth-child(odd) {
--tw-bg-opacity: 1;
background-color: rgb(245 245 245 / var(--tw-bg-opacity));
}
.even\:bg-neutral-50:nth-child(even) {
--tw-bg-opacity: 1;
background-color: rgb(250 250 250 / var(--tw-bg-opacity));
}
.hover\:scale-110:hover {
--tw-scale-x: 1.1;
--tw-scale-y: 1.1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.hover\:text-sky-800:hover {
--tw-text-opacity: 1;
color: rgb(7 89 133 / var(--tw-text-opacity));
}
.hover\:shadow-xl:hover {
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}

View File

@ -11,27 +11,28 @@ datasource db {
}
model Thread {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
title String
post String
imageName String
title String
post String
imageName String
posts Post[]
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
comment String
imageName String?
comment String
imageName String?
replyingTo Int?
post Thread @relation(fields: [postId], references: [id])
postId Int
post Thread @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}