Merge pull request #79 from Irev-Dev/dev
Monster PR, almost total rebuild
This commit was merged in pull request #79.
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -15,5 +15,9 @@
|
||||
"./web/src/pages",
|
||||
"./web/src/index.js",
|
||||
"./web/src/Routes.js",
|
||||
],
|
||||
"cSpell.words": [
|
||||
"Uploader",
|
||||
"redwoodjs"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Migration `20201009213512-create-posts`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/10/2020, 8:35:12 AM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "Post" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"body" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration ..20201009213512-create-posts
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,0 +1,18 @@
|
||||
+datasource DS {
|
||||
+ // optionally set multiple providers
|
||||
+ // example: provider = ["sqlite", "postgresql"]
|
||||
+ provider = "sqlite"
|
||||
+ url = "***"
|
||||
+}
|
||||
+
|
||||
+generator client {
|
||||
+ provider = "prisma-client-js"
|
||||
+ binaryTargets = "native"
|
||||
+}
|
||||
+
|
||||
+model Post {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ title String
|
||||
+ body String
|
||||
+ createdAt DateTime @default(now())
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateSource",
|
||||
"source": "DS"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "DS"
|
||||
},
|
||||
"argument": "provider",
|
||||
"value": "\"sqlite\""
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "DS"
|
||||
},
|
||||
"argument": "url",
|
||||
"value": "\"***\""
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Post"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Post",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Post",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Post",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Post",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Post",
|
||||
"field": "title",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Post",
|
||||
"field": "body",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Post",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Post",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Post",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
# Migration `20201011043647-create-parts`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/11/2020, 3:36:47 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "Part" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"mainImage" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201009213512-create-posts..20201011043647-create-parts
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,9 +1,9 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -15,4 +15,14 @@
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
+
|
||||
+model Part {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ title String
|
||||
+ description String // markdown string
|
||||
+ mainImage String // link to cloudinary
|
||||
+ createdAt DateTime @default(now())
|
||||
+ // userId
|
||||
+ //likes, comments, reactions
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Part"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "title",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "description",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "mainImage",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# Migration `20201011052155-add-code-to-part`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/11/2020, 4:21:55 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Part" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL DEFAULT '// Welcome to Cascade Studio! Here are some useful functions:
|
||||
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
|
||||
// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()
|
||||
// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),
|
||||
// FilletEdges(), ChamferEdges(),
|
||||
// Slider(), Button(), Checkbox()
|
||||
let holeRadius = Slider("Radius", 30 , 20 , 40);
|
||||
let sphere = Sphere(50);
|
||||
let cylinderZ = Cylinder(holeRadius, 200, true);/nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));
|
||||
let cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));/nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));
|
||||
|
||||
Translate([-25, 0, 40], Text3D("Hi!"));/n// Don''t forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!',
|
||||
"mainImage" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO "new_Part" ("id", "title", "description", "mainImage", "createdAt") SELECT "id", "title", "description", "mainImage", "createdAt" FROM "Part";
|
||||
DROP TABLE "Part";
|
||||
ALTER TABLE "new_Part" RENAME TO "Part";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201011043647-create-parts..20201011052155-add-code-to-part
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,9 +1,9 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -20,8 +20,9 @@
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
+ code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);/nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));/nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));/n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
```
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);/nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));/nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));/n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "code",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "code"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "code"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "\"// Welcome to Cascade Studio! Here are some useful functions:\\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\\n// FilletEdges(), ChamferEdges(),\\n// Slider(), Button(), Checkbox()\\nlet holeRadius = Slider(\\\"Radius\\\", 30 , 20 , 40);\\nlet sphere = Sphere(50);\\nlet cylinderZ = Cylinder(holeRadius, 200, true);/nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));/nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\\n\\nTranslate([-25, 0, 40], Text3D(\\\"Hi!\\\"));/n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
# Migration `20201011082558-add-code-not-needed-upon-create`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/11/2020, 7:25:58 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Part" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL DEFAULT '// Welcome to Cascade Studio! Here are some useful functions:
|
||||
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
|
||||
// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()
|
||||
// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),
|
||||
// FilletEdges(), ChamferEdges(),
|
||||
// Slider(), Button(), Checkbox()
|
||||
let holeRadius = Slider("Radius", 30 , 20 , 40);
|
||||
let sphere = Sphere(50);
|
||||
let cylinderZ = Cylinder(holeRadius, 200, true);
|
||||
let cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));
|
||||
let cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));
|
||||
Translate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));
|
||||
|
||||
Translate([-25, 0, 40], Text3D("Hi!"));
|
||||
// Don''t forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!',
|
||||
"mainImage" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO "new_Part" ("id", "title", "description", "mainImage", "createdAt", "code") SELECT "id", "title", "description", "mainImage", "createdAt", "code" FROM "Part";
|
||||
DROP TABLE "Part";
|
||||
ALTER TABLE "new_Part" RENAME TO "Part";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201011052155-add-code-to-part..20201011082558-add-code-not-needed-upon-create
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,9 +1,9 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -20,9 +20,9 @@
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
- code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);/nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));/nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));/n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
+ code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
```
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "UpdateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "code"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"newValue": "\"// Welcome to Cascade Studio! Here are some useful functions:\\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\\n// FilletEdges(), ChamferEdges(),\\n// Slider(), Button(), Checkbox()\\nlet holeRadius = Slider(\\\"Radius\\\", 30 , 20 , 40);\\nlet sphere = Sphere(50);\\nlet cylinderZ = Cylinder(holeRadius, 200, true);\\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\\n\\nTranslate([-25, 0, 40], Text3D(\\\"Hi!\\\"));\\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
# Migration `20201011095227-create-contact`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/11/2020, 8:52:27 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "Contact" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201011082558-add-code-not-needed-upon-create..20201011095227-create-contact
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,9 +1,9 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -26,4 +26,12 @@
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
+
|
||||
+model Contact {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ name String
|
||||
+ email String
|
||||
+ message String
|
||||
+ createdAt DateTime @default(now())
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
datasource DS {
|
||||
// optionally set multiple providers
|
||||
// example: provider = ["sqlite", "postgresql"]
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Contact"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Contact",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Contact",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Contact",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Contact",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Contact",
|
||||
"field": "name",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Contact",
|
||||
"field": "email",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Contact",
|
||||
"field": "message",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Contact",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Contact",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Contact",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# Migration `20201018233330-add-simple-user-model`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/19/2020, 10:33:30 AM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"userName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"issuer" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"image" TEXT,
|
||||
"bio" TEXT
|
||||
)
|
||||
|
||||
CREATE UNIQUE INDEX "User.userName_unique" ON "User"("userName")
|
||||
|
||||
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
|
||||
|
||||
CREATE UNIQUE INDEX "User.issuer_unique" ON "User"("issuer")
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201011095227-create-contact..20201018233330-add-simple-user-model
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,9 +1,7 @@
|
||||
datasource DS {
|
||||
- // optionally set multiple providers
|
||||
- // example: provider = ["sqlite", "postgresql"]
|
||||
- provider = "sqlite"
|
||||
- url = "***"
|
||||
+ provider = ["sqlite", "postgresql"]
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -34,4 +32,17 @@
|
||||
email String
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
+
|
||||
+model User {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ userName String @unique
|
||||
+ email String @unique
|
||||
+ issuer String @unique
|
||||
+
|
||||
+ createdAt DateTime @default(now())
|
||||
+ updatedAt DateTime @updatedAt
|
||||
+
|
||||
+ image String? // url maybe id or file storage service? cloudinary?
|
||||
+ bio String? //mark down
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
datasource DS {
|
||||
provider = ["sqlite", "postgresql"]
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
userName String @unique
|
||||
email String @unique
|
||||
issuer String @unique
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "UpdateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "DS"
|
||||
},
|
||||
"argument": "provider",
|
||||
"newValue": "[\"sqlite\", \"postgresql\"]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "User"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "userName",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "userName"
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "email",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "email"
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "issuer",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "issuer"
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "updatedAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "updatedAt"
|
||||
},
|
||||
"directive": "updatedAt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "image",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "bio",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
# Migration `20201019072122-add-simplify-user-model`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 10/19/2020, 6:21:22 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
DROP INDEX "User.issuer_unique"
|
||||
|
||||
DROP INDEX "User.userName_unique"
|
||||
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"email" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"image" TEXT,
|
||||
"bio" TEXT
|
||||
);
|
||||
INSERT INTO "new_User" ("id", "email", "createdAt", "updatedAt", "image", "bio") SELECT "id", "email", "createdAt", "updatedAt", "image", "bio" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201018233330-add-simple-user-model..20201019072122-add-simplify-user-model
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,7 +1,7 @@
|
||||
datasource DS {
|
||||
provider = ["sqlite", "postgresql"]
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -34,12 +34,12 @@
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
model User {
|
||||
- id Int @id @default(autoincrement())
|
||||
- userName String @unique
|
||||
- email String @unique
|
||||
- issuer String @unique
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ email String @unique
|
||||
+ // userName String @unique
|
||||
+ // issuer String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
```
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
datasource DS {
|
||||
provider = ["sqlite", "postgresql"]
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
// userName String @unique
|
||||
// issuer String @unique
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "DeleteField",
|
||||
"model": "User",
|
||||
"field": "userName"
|
||||
},
|
||||
{
|
||||
"tag": "DeleteField",
|
||||
"model": "User",
|
||||
"field": "issuer"
|
||||
}
|
||||
]
|
||||
}
|
||||
158
api/prisma/migrations/20201101183848-db-init/README.md
Normal file
158
api/prisma/migrations/20201101183848-db-init/README.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Migration `20201101183848-db-init`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 11/2/2020, 5:38:48 AM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"image" TEXT,
|
||||
"bio" TEXT,
|
||||
PRIMARY KEY ("id")
|
||||
)
|
||||
|
||||
CREATE TABLE "Part" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"code" TEXT,
|
||||
"mainImage" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY ("id")
|
||||
)
|
||||
|
||||
CREATE TABLE "PartReaction" (
|
||||
"id" TEXT NOT NULL,
|
||||
"emote" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"partId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY ("partId") REFERENCES "Part"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY ("id")
|
||||
)
|
||||
|
||||
CREATE TABLE "Comment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"partId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY ("partId") REFERENCES "Part"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY ("id")
|
||||
)
|
||||
|
||||
CREATE UNIQUE INDEX "User.userName_unique" ON "User"("userName")
|
||||
|
||||
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
|
||||
|
||||
CREATE UNIQUE INDEX "Part.title_userId_unique" ON "Part"("title", "userId")
|
||||
|
||||
CREATE UNIQUE INDEX "PartReaction.emote_userId_partId_unique" ON "PartReaction"("emote", "userId", "partId")
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration ..20201101183848-db-init
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,0 +1,79 @@
|
||||
+datasource DS {
|
||||
+ provider = ["sqlite", "postgresql"]
|
||||
+ url = "***"
|
||||
+}
|
||||
+
|
||||
+generator client {
|
||||
+ provider = "prisma-client-js"
|
||||
+ binaryTargets = "native"
|
||||
+}
|
||||
+
|
||||
+// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
|
||||
+// enum Role {
|
||||
+// USER
|
||||
+// ADMIN
|
||||
+// }
|
||||
+
|
||||
+// enum PartType {
|
||||
+// CASCADESTUDIO
|
||||
+// JSCAD
|
||||
+// }
|
||||
+
|
||||
+model User {
|
||||
+ id String @id @default(uuid())
|
||||
+ userName String @unique // reffered to as userId in @relations
|
||||
+ email String @unique
|
||||
+ // role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
|
||||
+ // maybe let netlify handle roles for now.
|
||||
+ // role String @default("user")
|
||||
+
|
||||
+ createdAt DateTime @default(now())
|
||||
+ updatedAt DateTime @updatedAt
|
||||
+
|
||||
+ image String? // url maybe id or file storage service? cloudinary?
|
||||
+ bio String? //mark down
|
||||
+ Part Part[]
|
||||
+ Reaction PartReaction[]
|
||||
+ Comment Comment[]
|
||||
+}
|
||||
+
|
||||
+model Part {
|
||||
+ id String @id @default(uuid())
|
||||
+ title String
|
||||
+ description String? // markdown string
|
||||
+ code String?
|
||||
+ mainImage String? // link to cloudinary
|
||||
+ createdAt DateTime @default(now())
|
||||
+ updatedAt DateTime @updatedAt
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
+ userId String
|
||||
+
|
||||
+ Comment Comment[]
|
||||
+ Reaction PartReaction[]
|
||||
+ @@unique([title, userId])
|
||||
+}
|
||||
+
|
||||
+model PartReaction {
|
||||
+ id String @id @default(uuid())
|
||||
+ emote String // an emoji
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
+ userId String
|
||||
+ part Part @relation(fields: [partId], references: [id])
|
||||
+ partId String
|
||||
+
|
||||
+ createdAt DateTime @default(now())
|
||||
+ updatedAt DateTime @updatedAt
|
||||
+ @@unique([emote, userId, partId])
|
||||
+}
|
||||
+
|
||||
+model Comment {
|
||||
+ id String @id @default(uuid())
|
||||
+ text String // the comment, should I allow mark down?
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
+ userId String
|
||||
+ part Part @relation(fields: [partId], references: [id])
|
||||
+ partId String
|
||||
+
|
||||
+ createdAt DateTime @default(now())
|
||||
+ updatedAt DateTime @updatedAt
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
79
api/prisma/migrations/20201101183848-db-init/schema.prisma
Normal file
79
api/prisma/migrations/20201101183848-db-init/schema.prisma
Normal file
@@ -0,0 +1,79 @@
|
||||
datasource DS {
|
||||
provider = ["sqlite", "postgresql"]
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
|
||||
// enum Role {
|
||||
// USER
|
||||
// ADMIN
|
||||
// }
|
||||
|
||||
// enum PartType {
|
||||
// CASCADESTUDIO
|
||||
// JSCAD
|
||||
// }
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
userName String @unique // reffered to as userId in @relations
|
||||
email String @unique
|
||||
// role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
|
||||
// maybe let netlify handle roles for now.
|
||||
// role String @default("user")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
Part Part[]
|
||||
Reaction PartReaction[]
|
||||
Comment Comment[]
|
||||
}
|
||||
|
||||
model Part {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String? // markdown string
|
||||
code String?
|
||||
mainImage String? // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
Comment Comment[]
|
||||
Reaction PartReaction[]
|
||||
@@unique([title, userId])
|
||||
}
|
||||
|
||||
model PartReaction {
|
||||
id String @id @default(uuid())
|
||||
emote String // an emoji
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@unique([emote, userId, partId])
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(uuid())
|
||||
text String // the comment, should I allow mark down?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
839
api/prisma/migrations/20201101183848-db-init/steps.json
Normal file
839
api/prisma/migrations/20201101183848-db-init/steps.json
Normal file
@@ -0,0 +1,839 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateSource",
|
||||
"source": "DS"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "DS"
|
||||
},
|
||||
"argument": "provider",
|
||||
"value": "[\"sqlite\", \"postgresql\"]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "DS"
|
||||
},
|
||||
"argument": "url",
|
||||
"value": "\"***\""
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "User"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "id",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "uuid()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "userName",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "userName"
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "email",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "email"
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "updatedAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "updatedAt"
|
||||
},
|
||||
"directive": "updatedAt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "image",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "bio",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "Part",
|
||||
"type": "Part",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "Reaction",
|
||||
"type": "PartReaction",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "Comment",
|
||||
"type": "Comment",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Part"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "id",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "uuid()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "title",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "description",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "code",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "mainImage",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "updatedAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "updatedAt"
|
||||
},
|
||||
"directive": "updatedAt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "user",
|
||||
"type": "User",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[userId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Part",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "userId",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "Comment",
|
||||
"type": "Comment",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Part",
|
||||
"field": "Reaction",
|
||||
"type": "PartReaction",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Model",
|
||||
"model": "Part",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "",
|
||||
"value": "[title, userId]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "PartReaction"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "id",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "uuid()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "emote",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "user",
|
||||
"type": "User",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[userId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "userId",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "part",
|
||||
"type": "Part",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "part"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "part"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[partId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "part"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "partId",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "PartReaction",
|
||||
"field": "updatedAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "PartReaction",
|
||||
"field": "updatedAt"
|
||||
},
|
||||
"directive": "updatedAt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Model",
|
||||
"model": "PartReaction",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "",
|
||||
"value": "[emote, userId, partId]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Comment"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "id",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "uuid()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "text",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "user",
|
||||
"type": "User",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[userId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "userId",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "part",
|
||||
"type": "Part",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "part"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "part"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[partId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "part"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "partId",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Comment",
|
||||
"field": "updatedAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Comment",
|
||||
"field": "updatedAt"
|
||||
},
|
||||
"directive": "updatedAt"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Migration `20201105184423-add-name-to-user`
|
||||
|
||||
This migration has been generated by Kurt Hutten at 11/6/2020, 5:44:24 AM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
ALTER TABLE "User" ADD COLUMN "name" TEXT
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201101183848-db-init..20201105184423-add-name-to-user
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,7 +1,7 @@
|
||||
datasource DS {
|
||||
provider = ["sqlite", "postgresql"]
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -19,11 +19,12 @@
|
||||
// JSCAD
|
||||
// }
|
||||
model User {
|
||||
- id String @id @default(uuid())
|
||||
- userName String @unique // reffered to as userId in @relations
|
||||
- email String @unique
|
||||
+ id String @id @default(uuid())
|
||||
+ userName String @unique // reffered to as userId in @relations
|
||||
+ email String @unique
|
||||
+ name String?
|
||||
// role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
|
||||
// maybe let netlify handle roles for now.
|
||||
// role String @default("user")
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
datasource DS {
|
||||
provider = ["sqlite", "postgresql"]
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
|
||||
// enum Role {
|
||||
// USER
|
||||
// ADMIN
|
||||
// }
|
||||
|
||||
// enum PartType {
|
||||
// CASCADESTUDIO
|
||||
// JSCAD
|
||||
// }
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
userName String @unique // reffered to as userId in @relations
|
||||
email String @unique
|
||||
name String?
|
||||
// role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
|
||||
// maybe let netlify handle roles for now.
|
||||
// role String @default("user")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
Part Part[]
|
||||
Reaction PartReaction[]
|
||||
Comment Comment[]
|
||||
}
|
||||
|
||||
model Part {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String? // markdown string
|
||||
code String?
|
||||
mainImage String? // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
Comment Comment[]
|
||||
Reaction PartReaction[]
|
||||
@@unique([title, userId])
|
||||
}
|
||||
|
||||
model PartReaction {
|
||||
id String @id @default(uuid())
|
||||
emote String // an emoji
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@unique([emote, userId, partId])
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(uuid())
|
||||
text String // the comment, should I allow mark down?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "name",
|
||||
"type": "String",
|
||||
"arity": "Optional"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
# Prisma Migrate lockfile v1
|
||||
|
||||
20201009213512-create-posts
|
||||
20201011043647-create-parts
|
||||
20201011052155-add-code-to-part
|
||||
20201011082558-add-code-not-needed-upon-create
|
||||
20201011095227-create-contact
|
||||
20201018233330-add-simple-user-model
|
||||
20201019072122-add-simplify-user-model
|
||||
20201101183848-db-init
|
||||
20201105184423-add-name-to-user
|
||||
@@ -8,41 +8,73 @@ generator client {
|
||||
binaryTargets = "native"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
|
||||
// enum Role {
|
||||
// USER
|
||||
// ADMIN
|
||||
// }
|
||||
|
||||
model Part {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String // markdown string
|
||||
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||
mainImage String // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
// userId
|
||||
//likes, comments, reactions
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
// enum PartType {
|
||||
// CASCADESTUDIO
|
||||
// JSCAD
|
||||
// }
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
// userName String @unique
|
||||
// issuer String @unique
|
||||
id String @id @default(uuid())
|
||||
userName String @unique // reffered to as userId in @relations
|
||||
email String @unique
|
||||
name String?
|
||||
// role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
|
||||
// maybe let netlify handle roles for now.
|
||||
// role String @default("user")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
Part Part[]
|
||||
Reaction PartReaction[]
|
||||
Comment Comment[]
|
||||
}
|
||||
|
||||
model Part {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String? // markdown string
|
||||
code String?
|
||||
mainImage String? // link to cloudinary
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
Comment Comment[]
|
||||
Reaction PartReaction[]
|
||||
@@unique([title, userId])
|
||||
}
|
||||
|
||||
model PartReaction {
|
||||
id String @id @default(uuid())
|
||||
emote String // an emoji
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@unique([emote, userId, partId])
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(uuid())
|
||||
text String // the comment, should I allow mark down?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createUserInsecure } from 'src/services/users/users.js'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const handler = async (req, _context) => {
|
||||
const body = JSON.parse(req.body)
|
||||
@@ -61,10 +62,25 @@ export const handler = async (req, _context) => {
|
||||
// image: '',
|
||||
// bio: ''
|
||||
// }
|
||||
|
||||
const generateUniqueUserName = async (seed, count = 0) => {
|
||||
const isUnique = !(await db.user.findOne({
|
||||
where: { userName: seed },
|
||||
}))
|
||||
if(isUnique) {
|
||||
return seed
|
||||
}
|
||||
count += 1
|
||||
const newSeed = count === 1 ? `${seed}_${count}` : seed.slice(0,-1) + count
|
||||
return generateUniqueUserName(newSeed, count)
|
||||
}
|
||||
const userNameSeed = email.split('@')[0]
|
||||
const userName = await generateUniqueUserName(userNameSeed) // TODO maybe come up with a better default userName?
|
||||
const input = {
|
||||
email,
|
||||
bio: 'default bio'
|
||||
// full_name: user.user_metadata.full_name
|
||||
userName,
|
||||
name: user.user_metadata && user.user_metadata.full_name,
|
||||
id: user.id,
|
||||
}
|
||||
await createUserInsecure({input})
|
||||
|
||||
|
||||
35
api/src/graphql/comments.sdl.js
Normal file
35
api/src/graphql/comments.sdl.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export const schema = gql`
|
||||
type Comment {
|
||||
id: String!
|
||||
text: String!
|
||||
user: User!
|
||||
userId: String!
|
||||
part: Part!
|
||||
partId: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
comments: [Comment!]!
|
||||
comment(id: String!): Comment
|
||||
}
|
||||
|
||||
input CreateCommentInput {
|
||||
text: String!
|
||||
userId: String!
|
||||
partId: String!
|
||||
}
|
||||
|
||||
input UpdateCommentInput {
|
||||
text: String
|
||||
userId: String
|
||||
partId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createComment(input: CreateCommentInput!): Comment!
|
||||
updateComment(id: String!, input: UpdateCommentInput!): Comment!
|
||||
deleteComment(id: String!): Comment!
|
||||
}
|
||||
`
|
||||
@@ -1,29 +0,0 @@
|
||||
export const schema = gql`
|
||||
type Contact {
|
||||
id: Int!
|
||||
name: String!
|
||||
email: String!
|
||||
message: String!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
contacts: [Contact!]!
|
||||
}
|
||||
|
||||
input CreateContactInput {
|
||||
name: String!
|
||||
email: String!
|
||||
message: String!
|
||||
}
|
||||
|
||||
input UpdateContactInput {
|
||||
name: String
|
||||
email: String
|
||||
message: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createContact(input: CreateContactInput!): Contact
|
||||
}
|
||||
`
|
||||
38
api/src/graphql/partReactions.sdl.js
Normal file
38
api/src/graphql/partReactions.sdl.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export const schema = gql`
|
||||
type PartReaction {
|
||||
id: String!
|
||||
emote: String!
|
||||
user: User!
|
||||
userId: String!
|
||||
part: Part!
|
||||
partId: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
partReactions: [PartReaction!]!
|
||||
partReaction(id: String!): PartReaction
|
||||
}
|
||||
|
||||
input TogglePartReactionInput {
|
||||
emote: String!
|
||||
userId: String!
|
||||
partId: String!
|
||||
}
|
||||
|
||||
input UpdatePartReactionInput {
|
||||
emote: String
|
||||
userId: String
|
||||
partId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
togglePartReaction(input: TogglePartReactionInput!): PartReaction!
|
||||
updatePartReaction(
|
||||
id: String!
|
||||
input: UpdatePartReactionInput!
|
||||
): PartReaction!
|
||||
deletePartReaction(id: String!): PartReaction!
|
||||
}
|
||||
`
|
||||
@@ -1,23 +1,30 @@
|
||||
export const schema = gql`
|
||||
type Part {
|
||||
id: Int!
|
||||
id: String!
|
||||
title: String!
|
||||
description: String!
|
||||
code: String!
|
||||
mainImage: String!
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
user: User!
|
||||
userId: String!
|
||||
Comment: [Comment]!
|
||||
Reaction(userId: String): [PartReaction]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
parts: [Part!]!
|
||||
part(id: Int!): Part
|
||||
part(id: String!): Part
|
||||
partByUserAndTitle(userName: String! partTitle: String!): Part
|
||||
}
|
||||
|
||||
input CreatePartInput {
|
||||
title: String!
|
||||
description: String!
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
userId: String!
|
||||
}
|
||||
|
||||
input UpdatePartInput {
|
||||
@@ -25,11 +32,12 @@ export const schema = gql`
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
userId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createPart(input: CreatePartInput!): Part!
|
||||
updatePart(id: Int!, input: UpdatePartInput!): Part!
|
||||
deletePart(id: Int!): Part!
|
||||
updatePart(id: String!, input: UpdatePartInput!): Part!
|
||||
deletePart(id: String!): Part!
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
export const schema = gql`
|
||||
type Post {
|
||||
id: Int!
|
||||
title: String!
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
posts: [Post!]!
|
||||
post(id: Int!): Post
|
||||
}
|
||||
|
||||
input CreatePostInput {
|
||||
title: String!
|
||||
body: String!
|
||||
}
|
||||
|
||||
input UpdatePostInput {
|
||||
title: String
|
||||
body: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createPost(input: CreatePostInput!): Post!
|
||||
updatePost(id: Int!, input: UpdatePostInput!): Post!
|
||||
deletePost(id: Int!): Post!
|
||||
}
|
||||
`
|
||||
@@ -1,35 +1,45 @@
|
||||
export const schema = gql`
|
||||
type User {
|
||||
id: Int!
|
||||
id: String!
|
||||
userName: String!
|
||||
email: String!
|
||||
name: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
image: String
|
||||
bio: String
|
||||
Parts: [Part]!
|
||||
Part(partTitle: String): Part
|
||||
Reaction: [PartReaction]!
|
||||
Comment: [Comment]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
users: [User!]!
|
||||
user(id: Int!): User
|
||||
user(id: String!): User
|
||||
userName(userName: String!): User
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
userName: String!
|
||||
email: String!
|
||||
# issuer: String!
|
||||
name: String
|
||||
image: String
|
||||
bio: String
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
userName: String
|
||||
email: String
|
||||
# issuer: String
|
||||
name: String
|
||||
image: String
|
||||
bio: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(input: CreateUserInput!): User!
|
||||
updateUser(id: Int!, input: UpdateUserInput!): User!
|
||||
deleteUser(id: Int!): User!
|
||||
updateUser(id: String!, input: UpdateUserInput!): User!
|
||||
updateUserByUserName(userName: String!, input: UpdateUserInput!): User!
|
||||
deleteUser(id: String!): User!
|
||||
}
|
||||
`
|
||||
|
||||
43
api/src/lib/owner.js
Normal file
43
api/src/lib/owner.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const requireOwnership = async ({ userId, userName, partId } = {}) => {
|
||||
// IMPORTANT, don't forget to await this function, as it will only block
|
||||
// unwanted db actions if it has time to look up resources in the db.
|
||||
if (!context.currentUser) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
if(!userId && !userName && !partId) {
|
||||
throw new ForbiddenError("You don't have access to do that.")
|
||||
}
|
||||
|
||||
if(context.currentUser.roles?.includes('admin')) {
|
||||
return
|
||||
}
|
||||
|
||||
const netlifyUserId = context.currentUser?.sub
|
||||
if(userId && userId !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
|
||||
if(userName) {
|
||||
const user = await db.user.findOne({
|
||||
where: { userName },
|
||||
})
|
||||
|
||||
if(!user || user.id !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
}
|
||||
|
||||
if(partId) {
|
||||
const user = await db.part.findOne({
|
||||
where: { id: partId },
|
||||
}).user()
|
||||
|
||||
if(!user || user.id !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
38
api/src/services/comments/comments.js
Normal file
38
api/src/services/comments/comments.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { db } from 'src/lib/db'
|
||||
import { foreignKeyReplacement } from 'src/services/helpers'
|
||||
|
||||
export const comments = () => {
|
||||
return db.comment.findMany()
|
||||
}
|
||||
|
||||
export const comment = ({ id }) => {
|
||||
return db.comment.findOne({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createComment = ({ input }) => {
|
||||
return db.comment.create({
|
||||
data: foreignKeyReplacement(input),
|
||||
})
|
||||
}
|
||||
|
||||
export const updateComment = ({ id, input }) => {
|
||||
return db.comment.update({
|
||||
data: foreignKeyReplacement(input),
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteComment = ({ id }) => {
|
||||
return db.comment.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const Comment = {
|
||||
user: (_obj, { root }) =>
|
||||
db.comment.findOne({ where: { id: root.id } }).user(),
|
||||
part: (_obj, { root }) =>
|
||||
db.comment.findOne({ where: { id: root.id } }).part(),
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
import { contacts } from './contacts'
|
||||
import { comments } from './comments'
|
||||
*/
|
||||
|
||||
describe('contacts', () => {
|
||||
describe('comments', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
|
||||
const validate = (input) => {
|
||||
if (input.email && !input.email.match(/[^@]+@[^.]+\..+/)) {
|
||||
throw new UserInputError("Can't create new contact", {
|
||||
messages: {
|
||||
email: ['is not formatted like an email address'],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const contacts = () => {
|
||||
return db.contact.findMany()
|
||||
}
|
||||
|
||||
export const createContact = ({ input }) => {
|
||||
|
||||
validate(input)
|
||||
return db.contact.create({ data: input })
|
||||
}
|
||||
13
api/src/services/helpers.js
Normal file
13
api/src/services/helpers.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export const foreignKeyReplacement = (input) => {
|
||||
let output = input
|
||||
const foreignKeys = Object.keys(input).filter((k) => k.match(/Id$/))
|
||||
foreignKeys.forEach((key) => {
|
||||
const modelName = key.replace(/Id$/, '')
|
||||
const value = input[key]
|
||||
delete output[key]
|
||||
output = Object.assign(output, {
|
||||
[modelName]: { connect: { id: value } },
|
||||
})
|
||||
})
|
||||
return output
|
||||
}
|
||||
58
api/src/services/partReactions/partReactions.js
Normal file
58
api/src/services/partReactions/partReactions.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
import { db } from 'src/lib/db'
|
||||
import { foreignKeyReplacement } from 'src/services/helpers'
|
||||
|
||||
export const partReactions = () => {
|
||||
return db.partReaction.findMany()
|
||||
}
|
||||
|
||||
export const partReaction = ({ id }) => {
|
||||
return db.partReaction.findOne({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const togglePartReaction = async ({ input }) => {
|
||||
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
||||
requireAuth()
|
||||
await requireOwnership({userId: input?.userId})
|
||||
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
|
||||
if(!legalReactions.includes(input.emote)) {
|
||||
throw new UserInputError(`You can't react with '${input.emote}', only the following are allowed: ${legalReactions.join(', ')}`)
|
||||
}
|
||||
let dbPromise
|
||||
const inputClone = {...input} // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
|
||||
try{
|
||||
dbPromise = await db.partReaction.create({
|
||||
data: foreignKeyReplacement(input),
|
||||
})
|
||||
} catch(e) {
|
||||
dbPromise = db.partReaction.delete({
|
||||
where: { emote_userId_partId: inputClone},
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
|
||||
export const updatePartReaction = ({ id, input }) => {
|
||||
return db.partReaction.update({
|
||||
data: foreignKeyReplacement(input),
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deletePartReaction = ({ id }) => {
|
||||
return db.partReaction.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const PartReaction = {
|
||||
user: (_obj, { root }) =>
|
||||
db.partReaction.findOne({ where: { id: root.id } }).user(),
|
||||
part: (_obj, { root }) =>
|
||||
db.partReaction.findOne({ where: { id: root.id } }).part(),
|
||||
}
|
||||
9
api/src/services/partReactions/partReactions.test.js
Normal file
9
api/src/services/partReactions/partReactions.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
import { partReactions } from './partReactions'
|
||||
*/
|
||||
|
||||
describe('partReactions', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,8 @@
|
||||
import { db } from 'src/lib/db'
|
||||
import { foreignKeyReplacement } from 'src/services/helpers'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
import { user } from 'src/services/users/users'
|
||||
|
||||
export const parts = () => {
|
||||
return db.part.findMany()
|
||||
@@ -9,22 +13,52 @@ export const part = ({ id }) => {
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createPart = ({ input }) => {
|
||||
return db.part.create({
|
||||
data: input,
|
||||
export const partByUserAndTitle = async ({ userName, partTitle }) => {
|
||||
const user = await db.user.findOne({
|
||||
where: {
|
||||
userName
|
||||
}
|
||||
})
|
||||
return db.part.findOne({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: partTitle,
|
||||
userId: user.id,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePart = ({ id, input }) => {
|
||||
export const createPart = async ({ input }) => {
|
||||
requireAuth()
|
||||
return db.part.create({
|
||||
data: foreignKeyReplacement(input),
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePart = async ({ id, input }) => {
|
||||
requireAuth()
|
||||
await requireOwnership({partId: id})
|
||||
if(input.title) {
|
||||
input.title = input.title.replace(/([^a-zA-Z\d_:])/g, '-')
|
||||
}
|
||||
return db.part.update({
|
||||
data: input,
|
||||
data: foreignKeyReplacement(input),
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deletePart = ({ id }) => {
|
||||
requireAuth()
|
||||
return db.part.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const Part = {
|
||||
user: (_obj, { root }) => db.part.findOne({ where: { id: root.id } }).user(),
|
||||
Comment: (_obj, { root }) =>
|
||||
db.part.findOne({ where: { id: root.id } }).Comment(),
|
||||
Reaction: (_obj, { root }) =>
|
||||
db.part.findOne({ where: { id: root.id } }).Reaction({where: {userId: _obj.userId}}),
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { db } from 'src/lib/db'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
|
||||
export const posts = () => {
|
||||
return db.post.findMany()
|
||||
}
|
||||
|
||||
export const post = ({ id }) => {
|
||||
return db.post.findOne({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createPost = ({ input }) => {
|
||||
requireAuth()
|
||||
return db.post.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePost = ({ id, input }) => {
|
||||
requireAuth()
|
||||
return db.post.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deletePost = ({ id }) => {
|
||||
requireAuth()
|
||||
return db.post.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
import { posts } from './posts'
|
||||
*/
|
||||
|
||||
describe('posts', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { db } from 'src/lib/db'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
|
||||
export const users = () => {
|
||||
requireAuth({ role: 'admin' })
|
||||
@@ -7,17 +9,21 @@ export const users = () => {
|
||||
}
|
||||
|
||||
export const user = ({ id }) => {
|
||||
requireAuth()
|
||||
return db.user.findOne({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createUser = ({ input }) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
return createUserInsecure({input})
|
||||
export const userName = ({ userName }) => {
|
||||
return db.user.findOne({
|
||||
where: { userName },
|
||||
})
|
||||
}
|
||||
|
||||
export const createUser = ({ input }) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
createUserInsecure({input})
|
||||
}
|
||||
export const createUserInsecure = ({ input }) => {
|
||||
return db.user.create({
|
||||
data: input,
|
||||
@@ -32,9 +38,36 @@ export const updateUser = ({ id, input }) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUserByUserName = async ({ userName, input }) => {
|
||||
requireAuth()
|
||||
await requireOwnership({userName})
|
||||
if(input.userName) {
|
||||
input.userName = input.userName.replace(/([^a-zA-Z\d_:])/g, '-')
|
||||
}
|
||||
if(input.userName && ['new', 'edit', 'update'].includes(input.userName)) { //TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
||||
throw new UserInputError(`You've tried to used a protected word as you userName, try something other than `)
|
||||
}
|
||||
return db.user.update({
|
||||
data: input,
|
||||
where: { userName },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteUser = ({ id }) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
return db.user.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const User = {
|
||||
Parts: (_obj, { root }) => db.user.findOne({ where: { id: root.id } }).Part(),
|
||||
Part: (_obj, { root, ...rest }) => _obj.partTitle && db.part.findOne({where: { title_userId: {
|
||||
title: _obj.partTitle,
|
||||
userId: root.id,
|
||||
}}}),
|
||||
Reaction: (_obj, { root }) =>
|
||||
db.user.findOne({ where: { id: root.id } }).Reaction(),
|
||||
Comment: (_obj, { root }) =>
|
||||
db.user.findOne({ where: { id: root.id } }).Comment(),
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@redwoodjs/web": "^0.19.2",
|
||||
"cloudinary-react": "^1.6.7",
|
||||
"controlkit": "^0.1.9",
|
||||
"get-active-classes": "^0.0.11",
|
||||
"golden-layout": "^1.5.9",
|
||||
"jquery": "^3.5.1",
|
||||
"monaco-editor": "^0.20.0",
|
||||
|
||||
@@ -12,31 +12,39 @@ import { Router, Route, Private } from '@redwoodjs/router'
|
||||
const Routes = () => {
|
||||
return (
|
||||
<Router>
|
||||
{/* TODO add add min role to users and users/new */}
|
||||
<Private unauthenticated="home" role="admin">
|
||||
<Route path="/users" page={UsersPage} name="users" />
|
||||
<Route path="/users/new" page={NewUserPage} name="newUser" />
|
||||
</Private>
|
||||
<Private unauthenticated="home">
|
||||
<Route path="/users/{id:Int}/edit" page={EditUserPage} name="editUser" />
|
||||
</Private>
|
||||
<Route path="/users/{id:Int}" page={UserPage} name="user" />
|
||||
<Route path="/contact" page={ContactPage} name="contact" />
|
||||
<Route path="/parts/new" page={NewPartPage} name="newPart" />
|
||||
<Route path="/parts/{id:Int}/edit" page={EditPartPage} name="editPart" />
|
||||
<Route path="/parts/{id:Int}/ide" page={IdePartPage} name="partIde" />
|
||||
<Route path="/parts/{id:Int}" page={PartPage} name="part" />
|
||||
<Route path="/parts" page={PartsPage} name="parts" />
|
||||
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
|
||||
<Private unauthenticated="home">
|
||||
<Route path="/admin/posts/new" page={NewPostPage} name="newPost" />
|
||||
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
|
||||
<Route path="/admin/posts/{id:Int}" page={PostPage} name="post" />
|
||||
<Route path="/admin/posts" page={PostsPage} name="posts" />
|
||||
</Private>
|
||||
<Route path="/about" page={AboutPage} name="about" />
|
||||
<Route path="/" page={PartsPage} name="home" />
|
||||
{/* <Route path="/blah/*" page={PartsPage} name="home" /> */}
|
||||
<Route notfound page={NotFoundPage} />
|
||||
|
||||
{/* Ownership enforced routes */}
|
||||
<Route path="/u/{userName}/new" page={NewPart2Page} name="newPart2" />
|
||||
<Route path="/u/{userName}/edit" page={EditUser2Page} name="editUser2" />
|
||||
<Route path="/u/{userName}/{partTitle}/edit" page={EditPart2Page} name="editPart2" />
|
||||
{/* End ownership enforced routes */}
|
||||
|
||||
<Route path="/u/{userName}" page={User2Page} name="user2" />
|
||||
<Route path="/u/{userName}/{partTitle}" page={Part2Page} name="part2" />
|
||||
{/* <Route path="/u/{userName}/{partTitle}/ide" page={Part2Page} name="part2" /> */}
|
||||
|
||||
{/* GENERATED ROUTES BELOW, probably going to clean these up and delete most of them, but the CRUD functionality is useful for now */}
|
||||
{/* All private by default for safety and because the routes that are left after clean up will probably be admin pages */}
|
||||
<Private unauthenticated="home" role="admin">
|
||||
<Route path="/part-reactions/new" page={NewPartReactionPage} name="newPartReaction" />
|
||||
<Route path="/part-reactions/{id}/edit" page={EditPartReactionPage} name="editPartReaction" />
|
||||
<Route path="/part-reactions/{id}" page={PartReactionPage} name="partReaction" />
|
||||
<Route path="/part-reactions" page={PartReactionsPage} name="partReactions" />
|
||||
<Route path="/parts/new" page={NewPartPage} name="newPart" />
|
||||
<Route path="/parts/{id}/edit" page={EditPartPage} name="editPart" />
|
||||
<Route path="/parts/{id}" page={PartPage} name="part" />
|
||||
<Route path="/comments/new" page={NewCommentPage} name="newComment" />
|
||||
<Route path="/comments/{id}/edit" page={EditCommentPage} name="editComment" />
|
||||
<Route path="/comments/{id}" page={CommentPage} name="comment" />
|
||||
<Route path="/comments" page={CommentsPage} name="comments" />
|
||||
<Route path="/users/new" page={NewUserPage} name="newUser" />
|
||||
<Route path="/users/{id}/edit" page={EditUserPage} name="editUser" />
|
||||
<Route path="/users/{id}" page={UserPage} name="user" />
|
||||
<Route path="/users" page={UsersPage} name="users" />
|
||||
</Private>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
const BlogPost = ({ post }) => {
|
||||
return (
|
||||
<article>
|
||||
<header>
|
||||
<h2>
|
||||
<Link to={routes.blogPost({ id: post.id })}>{post.title}</Link>
|
||||
</h2>
|
||||
</header>
|
||||
<div>{post.body}</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
@@ -1,7 +0,0 @@
|
||||
import BlogPost from './BlogPost'
|
||||
|
||||
export const generated = () => {
|
||||
return <BlogPost />
|
||||
}
|
||||
|
||||
export default { title: 'Components/BlogPost' }
|
||||
@@ -1,22 +0,0 @@
|
||||
import BlogPost from 'src/components/BlogPost'
|
||||
|
||||
export const QUERY = gql`
|
||||
query BlogPostQuery($id: Int!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
|
||||
export const Success = ({ post }) => {
|
||||
return <BlogPost post={post} />
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import BlogPost from 'src/components/BlogPost'
|
||||
|
||||
export const QUERY = gql`
|
||||
query BlogPostsQuery {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
|
||||
export const Success = ({ posts }) => {
|
||||
return posts.map((post) => <BlogPost key={post.id} post={post} />)
|
||||
}
|
||||
23
web/src/components/Breadcrumb/Breadcrumb.js
Normal file
23
web/src/components/Breadcrumb/Breadcrumb.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getActiveClasses } from "get-active-classes"
|
||||
|
||||
import InputText from 'src/components/InputText'
|
||||
|
||||
const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
|
||||
return (
|
||||
<h3 className={getActiveClasses("text-2xl font-roboto", className)}>
|
||||
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">.</div>
|
||||
<span className={getActiveClasses({"text-gray-500": !onPartTitleChange, 'text-gray-400': onPartTitleChange})}>
|
||||
{userName}
|
||||
</span>
|
||||
<div className="w-1 inline-block bg-gray-400 text-gray-400 mx-3 transform -skew-x-20" >.</div>
|
||||
<InputText
|
||||
value={partTitle}
|
||||
onChange={onPartTitleChange}
|
||||
isEditable={onPartTitleChange}
|
||||
className={getActiveClasses("text-indigo-800 text-2xl", {'-ml-2': !onPartTitleChange})}
|
||||
/>
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
export default Breadcrumb
|
||||
7
web/src/components/Breadcrumb/Breadcrumb.stories.js
Normal file
7
web/src/components/Breadcrumb/Breadcrumb.stories.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Breadcrumb from './Breadcrumb'
|
||||
|
||||
export const generated = () => {
|
||||
return <Breadcrumb />
|
||||
}
|
||||
|
||||
export default { title: 'Components/Breadcrumb' }
|
||||
@@ -1,11 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import BlogLayout from './BlogLayout'
|
||||
import Breadcrumb from './Breadcrumb'
|
||||
|
||||
describe('BlogLayout', () => {
|
||||
describe('Breadcrumb', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<BlogLayout />)
|
||||
render(<Breadcrumb />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
22
web/src/components/Button/Button.js
Normal file
22
web/src/components/Button/Button.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getActiveClasses } from 'get-active-classes'
|
||||
import Svg from 'src/components/Svg'
|
||||
|
||||
const Button = ({onClick, iconName, children, className, shouldAnimateHover, disabled}) => {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
className={getActiveClasses(
|
||||
"flex items-center bg-opacity-50 rounded-xl p-2 px-6 text-indigo-600",
|
||||
{'mx-px transform hover:-translate-y-px transition-all duration-150': shouldAnimateHover && !disabled},
|
||||
className,
|
||||
{"bg-gray-300 shadow-none hover:shadow-none": disabled},
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
<Svg className="w-6 ml-4" name={iconName} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
10
web/src/components/Button/Button.stories.js
Normal file
10
web/src/components/Button/Button.stories.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Button from './Button'
|
||||
|
||||
export const generated = () => {
|
||||
return <>
|
||||
button with icon
|
||||
<Button>click Me </Button>
|
||||
</>
|
||||
}
|
||||
|
||||
export default { title: 'Components/Button' }
|
||||
@@ -1,11 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import BlogPost from './BlogPost'
|
||||
import Button from './Button'
|
||||
|
||||
describe('BlogPost', () => {
|
||||
describe('Button', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<BlogPost />)
|
||||
render(<Button />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
|
||||
const DELETE_POST_MUTATION = gql`
|
||||
mutation DeletePostMutation($id: Int!) {
|
||||
deletePost(id: $id) {
|
||||
const DELETE_COMMENT_MUTATION = gql`
|
||||
mutation DeleteCommentMutation($id: String!) {
|
||||
deleteComment(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -29,18 +29,18 @@ const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const Post = ({ post }) => {
|
||||
const Comment = ({ comment }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
|
||||
const [deleteComment] = useMutation(DELETE_COMMENT_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.posts())
|
||||
addMessage('Post deleted.', { classes: 'rw-flash-success' })
|
||||
navigate(routes.comments())
|
||||
addMessage('Comment deleted.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id) => {
|
||||
if (confirm('Are you sure you want to delete post ' + id + '?')) {
|
||||
deletePost({ variables: { id } })
|
||||
if (confirm('Are you sure you want to delete comment ' + id + '?')) {
|
||||
deleteComment({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,33 +49,41 @@ const Post = ({ post }) => {
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Post {post.id} Detail
|
||||
Comment {comment.id} Detail
|
||||
</h2>
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>{post.id}</td>
|
||||
<td>{comment.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{post.title}</td>
|
||||
<th>Text</th>
|
||||
<td>{comment.text}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Body</th>
|
||||
<td>{post.body}</td>
|
||||
<th>User id</th>
|
||||
<td>{comment.userId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Part id</th>
|
||||
<td>{comment.partId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
<td>{timeTag(post.createdAt)}</td>
|
||||
<td>{timeTag(comment.createdAt)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Updated at</th>
|
||||
<td>{timeTag(comment.updatedAt)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav className="rw-button-group">
|
||||
<Link
|
||||
to={routes.editPost({ id: post.id })}
|
||||
to={routes.editComment({ id: comment.id })}
|
||||
className="rw-button rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
@@ -83,7 +91,7 @@ const Post = ({ post }) => {
|
||||
<a
|
||||
href="#"
|
||||
className="rw-button rw-button-red"
|
||||
onClick={() => onDeleteClick(post.id)}
|
||||
onClick={() => onDeleteClick(comment.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
@@ -92,4 +100,4 @@ const Post = ({ post }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Post
|
||||
export default Comment
|
||||
22
web/src/components/CommentCell/CommentCell.js
Normal file
22
web/src/components/CommentCell/CommentCell.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Comment from 'src/components/Comment'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_COMMENT_BY_ID($id: String!) {
|
||||
comment: comment(id: $id) {
|
||||
id
|
||||
text
|
||||
userId
|
||||
partId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Comment not found</div>
|
||||
|
||||
export const Success = ({ comment }) => {
|
||||
return <Comment comment={comment} />
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
Submit,
|
||||
} from '@redwoodjs/forms'
|
||||
|
||||
const PostForm = (props) => {
|
||||
const CommentForm = (props) => {
|
||||
const onSubmit = (data) => {
|
||||
props.onSave(data, props?.post?.id)
|
||||
props.onSave(data, props?.comment?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -23,36 +23,52 @@ const PostForm = (props) => {
|
||||
/>
|
||||
|
||||
<Label
|
||||
name="title"
|
||||
name="text"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Title
|
||||
Text
|
||||
</Label>
|
||||
<TextField
|
||||
name="title"
|
||||
defaultValue={props.post?.title}
|
||||
name="text"
|
||||
defaultValue={props.comment?.text}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="title" className="rw-field-error" />
|
||||
<FieldError name="text" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="body"
|
||||
name="userId"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Body
|
||||
User id
|
||||
</Label>
|
||||
<TextField
|
||||
name="body"
|
||||
defaultValue={props.post?.body}
|
||||
name="userId"
|
||||
defaultValue={props.comment?.userId}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="body" className="rw-field-error" />
|
||||
<FieldError name="userId" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="partId"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Part id
|
||||
</Label>
|
||||
<TextField
|
||||
name="partId"
|
||||
defaultValue={props.comment?.partId}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="partId" className="rw-field-error" />
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||
@@ -64,4 +80,4 @@ const PostForm = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default PostForm
|
||||
export default CommentForm
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
const DELETE_POST_MUTATION = gql`
|
||||
mutation DeletePostMutation($id: Int!) {
|
||||
deletePost(id: $id) {
|
||||
const DELETE_COMMENT_MUTATION = gql`
|
||||
mutation DeleteCommentMutation($id: String!) {
|
||||
deleteComment(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -35,17 +35,17 @@ const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const PostsList = ({ posts }) => {
|
||||
const CommentsList = ({ comments }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
|
||||
const [deleteComment] = useMutation(DELETE_COMMENT_MUTATION, {
|
||||
onCompleted: () => {
|
||||
addMessage('Post deleted.', { classes: 'rw-flash-success' })
|
||||
addMessage('Comment deleted.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id) => {
|
||||
if (confirm('Are you sure you want to delete post ' + id + '?')) {
|
||||
deletePost({ variables: { id }, refetchQueries: ['POSTS'] })
|
||||
if (confirm('Are you sure you want to delete comment ' + id + '?')) {
|
||||
deleteComment({ variables: { id }, refetchQueries: ['COMMENTS'] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,40 +55,44 @@ const PostsList = ({ posts }) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Title</th>
|
||||
<th>Body</th>
|
||||
<th>Text</th>
|
||||
<th>User id</th>
|
||||
<th>Part id</th>
|
||||
<th>Created at</th>
|
||||
<th>Updated at</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{posts.map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td>{truncate(post.id)}</td>
|
||||
<td>{truncate(post.title)}</td>
|
||||
<td>{truncate(post.body)}</td>
|
||||
<td>{timeTag(post.createdAt)}</td>
|
||||
{comments.map((comment) => (
|
||||
<tr key={comment.id}>
|
||||
<td>{truncate(comment.id)}</td>
|
||||
<td>{truncate(comment.text)}</td>
|
||||
<td>{truncate(comment.userId)}</td>
|
||||
<td>{truncate(comment.partId)}</td>
|
||||
<td>{timeTag(comment.createdAt)}</td>
|
||||
<td>{timeTag(comment.updatedAt)}</td>
|
||||
<td>
|
||||
<nav className="rw-table-actions">
|
||||
<Link
|
||||
to={routes.post({ id: post.id })}
|
||||
title={'Show post ' + post.id + ' detail'}
|
||||
to={routes.comment({ id: comment.id })}
|
||||
title={'Show comment ' + comment.id + ' detail'}
|
||||
className="rw-button rw-button-small"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editPost({ id: post.id })}
|
||||
title={'Edit post ' + post.id}
|
||||
to={routes.editComment({ id: comment.id })}
|
||||
title={'Edit comment ' + comment.id}
|
||||
className="rw-button rw-button-small rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<a
|
||||
href="#"
|
||||
title={'Delete post ' + post.id}
|
||||
title={'Delete comment ' + comment.id}
|
||||
className="rw-button rw-button-small rw-button-red"
|
||||
onClick={() => onDeleteClick(post.id)}
|
||||
onClick={() => onDeleteClick(comment.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
@@ -102,4 +106,4 @@ const PostsList = ({ posts }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default PostsList
|
||||
export default CommentsList
|
||||
33
web/src/components/CommentsCell/CommentsCell.js
Normal file
33
web/src/components/CommentsCell/CommentsCell.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
import Comments from 'src/components/Comments'
|
||||
|
||||
export const QUERY = gql`
|
||||
query COMMENTS {
|
||||
comments {
|
||||
id
|
||||
text
|
||||
userId
|
||||
partId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => {
|
||||
return (
|
||||
<div className="rw-text-center">
|
||||
{'No comments yet. '}
|
||||
<Link to={routes.newComment()} className="rw-link">
|
||||
{'Create one?'}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Success = ({ comments }) => {
|
||||
return <Comments comments={comments} />
|
||||
}
|
||||
60
web/src/components/EditCommentCell/EditCommentCell.js
Normal file
60
web/src/components/EditCommentCell/EditCommentCell.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import CommentForm from 'src/components/CommentForm'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_COMMENT_BY_ID($id: String!) {
|
||||
comment: comment(id: $id) {
|
||||
id
|
||||
text
|
||||
userId
|
||||
partId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
const UPDATE_COMMENT_MUTATION = gql`
|
||||
mutation UpdateCommentMutation($id: String!, $input: UpdateCommentInput!) {
|
||||
updateComment(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Success = ({ comment }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [updateComment, { loading, error }] = useMutation(
|
||||
UPDATE_COMMENT_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
navigate(routes.comments())
|
||||
addMessage('Comment updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (input, id) => {
|
||||
updateComment({ variables: { id, input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Edit Comment {comment.id}
|
||||
</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<CommentForm
|
||||
comment={comment}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMutation } from '@redwoodjs/web'
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PartForm from 'src/components/PartForm'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_PART_BY_ID($id: Int!) {
|
||||
query FIND_PART_BY_ID($id: String!) {
|
||||
part: part(id: $id) {
|
||||
id
|
||||
title
|
||||
@@ -10,14 +11,15 @@ export const QUERY = gql`
|
||||
code
|
||||
mainImage
|
||||
createdAt
|
||||
updatedAt
|
||||
userId
|
||||
}
|
||||
}
|
||||
`
|
||||
const UPDATE_PART_MUTATION = gql`
|
||||
mutation UpdatePartMutation($id: Int!, $input: UpdatePartInput!) {
|
||||
mutation UpdatePartMutation($id: String!, $input: UpdatePartInput!) {
|
||||
updatePart(id: $id, input: $input) {
|
||||
id
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -25,11 +27,26 @@ const UPDATE_PART_MUTATION = gql`
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Success = ({ part }) => {
|
||||
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION)
|
||||
const { addMessage } = useFlash()
|
||||
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.parts())
|
||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onSave = (input, id) => updatePart({ variables: { id, input } })
|
||||
const onSave = (input, id) => {
|
||||
updatePart({ variables: { id, input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<PartForm part={part} onSave={onSave} error={error} loading={loading} />
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">Edit Part {part.id}</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PartForm part={part} onSave={onSave} error={error} loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PartReactionForm from 'src/components/PartReactionForm'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_PART_REACTION_BY_ID($id: String!) {
|
||||
partReaction: partReaction(id: $id) {
|
||||
id
|
||||
emote
|
||||
userId
|
||||
partId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
const UPDATE_PART_REACTION_MUTATION = gql`
|
||||
mutation UpdatePartReactionMutation(
|
||||
$id: String!
|
||||
$input: UpdatePartReactionInput!
|
||||
) {
|
||||
updatePartReaction(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Success = ({ partReaction }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [updatePartReaction, { loading, error }] = useMutation(
|
||||
UPDATE_PART_REACTION_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
navigate(routes.partReactions())
|
||||
addMessage('PartReaction updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (input, id) => {
|
||||
updatePartReaction({ variables: { id, input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Edit PartReaction {partReaction.id}
|
||||
</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PartReactionForm
|
||||
partReaction={partReaction}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PostForm from 'src/components/PostForm'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_POST_BY_ID($id: Int!) {
|
||||
post: post(id: $id) {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
const UPDATE_POST_MUTATION = gql`
|
||||
mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
|
||||
updatePost(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Success = ({ post }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.posts())
|
||||
addMessage('Post updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onSave = (input, id) => {
|
||||
updatePost({ variables: { id, input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">Edit Post {post.id}</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PostForm post={post} onSave={onSave} error={error} loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
web/src/components/EditUser2Cell/EditUser2Cell.js
Normal file
55
web/src/components/EditUser2Cell/EditUser2Cell.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
|
||||
import UserProfile from 'src/components/UserProfile'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_USER_BY_ID($userName: String!) {
|
||||
user: userName(userName: $userName) {
|
||||
id
|
||||
userName
|
||||
name
|
||||
createdAt
|
||||
updatedAt
|
||||
image
|
||||
bio
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UPDATE_USER_MUTATION = gql`
|
||||
mutation UpdateUserMutation($userName: String!, $input: UpdateUserInput!) {
|
||||
updateUserByUserName(userName: $userName, input: $input) {
|
||||
id
|
||||
userName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
|
||||
export const Success = ({ user }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
|
||||
onCompleted: ({updateUserByUserName}) => {
|
||||
navigate(routes.user2({userName: updateUserByUserName.userName}))
|
||||
addMessage('User updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onSave = (userName, input) => {
|
||||
updateUser({ variables: { userName, input } })
|
||||
}
|
||||
|
||||
return <UserProfile
|
||||
user={user}
|
||||
onSave={onSave}
|
||||
loading={loading}
|
||||
error={error}
|
||||
isEditable
|
||||
/>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
blogPosts: {
|
||||
editUser2: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Loading, Empty, Failure, Success } from './BlogPostsCell'
|
||||
import { standard } from './BlogPostsCell.mock'
|
||||
import { Loading, Empty, Failure, Success } from './EditUser2Cell'
|
||||
import { standard } from './EditUser2Cell.mock'
|
||||
|
||||
export const loading = () => {
|
||||
return Loading ? <Loading /> : null
|
||||
@@ -17,4 +17,4 @@ export const success = () => {
|
||||
return Success ? <Success {...standard()} /> : null
|
||||
}
|
||||
|
||||
export default { title: 'Cells/BlogPostsCell' }
|
||||
export default { title: 'Cells/EditUser2Cell' }
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from '@redwoodjs/testing'
|
||||
import { Loading, Empty, Failure, Success } from './BlogPostsCell'
|
||||
import { standard } from './BlogPostsCell.mock'
|
||||
import { Loading, Empty, Failure, Success } from './EditUser2Cell'
|
||||
import { standard } from './EditUser2Cell.mock'
|
||||
|
||||
describe('BlogPostsCell', () => {
|
||||
describe('EditUser2Cell', () => {
|
||||
test('Loading renders successfully', () => {
|
||||
render(<Loading />)
|
||||
// Use screen.debug() to see output
|
||||
@@ -20,7 +20,7 @@ describe('BlogPostsCell', () => {
|
||||
})
|
||||
|
||||
test('Success renders successfully', async () => {
|
||||
render(<Success blogPosts={standard().blogPosts} />)
|
||||
render(<Success editUser2={standard().editUser2} />)
|
||||
expect(screen.getByText(/42/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -3,9 +3,10 @@ import { navigate, routes } from '@redwoodjs/router'
|
||||
import UserForm from 'src/components/UserForm'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_USER_BY_ID($id: Int!) {
|
||||
query FIND_USER_BY_ID($id: String!) {
|
||||
user: user(id: $id) {
|
||||
id
|
||||
userName
|
||||
email
|
||||
createdAt
|
||||
updatedAt
|
||||
@@ -15,7 +16,7 @@ export const QUERY = gql`
|
||||
}
|
||||
`
|
||||
const UPDATE_USER_MUTATION = gql`
|
||||
mutation UpdateUserMutation($id: Int!, $input: UpdateUserInput!) {
|
||||
mutation UpdateUserMutation($id: String!, $input: UpdateUserInput!) {
|
||||
updateUser(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { useState } from 'react'
|
||||
import Fab from '@material-ui/core/Fab'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { getActiveClasses } from "get-active-classes"
|
||||
import Popover from '@material-ui/core/Popover'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
|
||||
import Svg from 'src/components/Svg'
|
||||
|
||||
const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
|
||||
const emojiMenu = ['❤️', '👍', '😄', '🙌']
|
||||
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
|
||||
const noEmotes =[{
|
||||
emoji: '❤️',
|
||||
count: 0,
|
||||
}]
|
||||
|
||||
const EmojiReaction = ({ emotes, callback = () => {} }) => {
|
||||
const textShadow = {textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'}
|
||||
|
||||
const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => {
|
||||
const { currentUser } = useAuth()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [popoverId, setPopoverId] = useState(undefined)
|
||||
@@ -32,45 +41,67 @@ const EmojiReaction = ({ emotes, callback = () => {} }) => {
|
||||
}
|
||||
|
||||
const handleEmojiClick = (emoji) => {
|
||||
callback(emoji)
|
||||
// TODO handle user not signed in better, maybe open up a modal, I danno think about it.
|
||||
currentUser && onEmote(emoji)
|
||||
closePopover()
|
||||
}
|
||||
|
||||
return [
|
||||
<div className="flex justify-between">
|
||||
<Fab size="medium" variant="round" aria-describedby={popoverId} onClick={togglePopover}>
|
||||
<div className="bg-gray-200 border-2 m-px border-gray-300 text-gray-500 absolute inset-0 rounded-full flex justify-center items-center">
|
||||
<Svg name="dots-vertical" />
|
||||
return (
|
||||
<>
|
||||
<div className={getActiveClasses("h-10 relative overflow-hidden py-4", className)}>
|
||||
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
|
||||
<div className="h-8 w-8 relative" aria-describedby={popoverId} onClick={togglePopover}>
|
||||
<button
|
||||
className="bg-gray-200 border-2 m-px w-full h-full border-gray-300 rounded-full flex justify-center items-center shadow-md hover:shadow-lg hover:border-indigo-200 transform hover:-translate-y-px transition-all duration-150"
|
||||
>
|
||||
<Svg className="h-8 w-8 pt-px mt-px text-gray-500" name="dots-vertical" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Fab>
|
||||
|
||||
<div>
|
||||
{emotes.map((emote, i) => (
|
||||
<IconButton key={`${emote.emoji}--${i}`} onClick={() => handleEmojiClick(emote.emoji)}>
|
||||
{emote.emoji} <span>{emote.count}</span>
|
||||
</IconButton>
|
||||
))}
|
||||
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
|
||||
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
|
||||
<span
|
||||
className={getActiveClasses(
|
||||
"rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150 border-indigo-400",
|
||||
{'border': currentUser && userEmotes?.includes(emote.emoji)}
|
||||
)}
|
||||
style={textShadow}
|
||||
key={`${emote.emoji}--${i}`}
|
||||
onClick={() => handleEmojiClick(emote.emoji)}
|
||||
>
|
||||
<span className="text-lg pr-1">{emote.emoji}</span><span className="text-sm font-ropa-sans">{emote.count}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={closePopover}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
>
|
||||
{emojiMenu.map((emoji, i) => (
|
||||
<IconButton key={`${emoji}-${i}}`} onClick={() => handleEmojiClick(emoji)}>{emoji}</IconButton>
|
||||
))}
|
||||
</Popover>,
|
||||
]
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={closePopover}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<div className="py-2 mt-2">
|
||||
{emojiMenu.map((emoji, i) => (
|
||||
<button
|
||||
className="p-2"
|
||||
style={textShadow}
|
||||
key={`${emoji}-${i}}`}
|
||||
onClick={() => handleEmojiClick(emoji)}
|
||||
>{emoji}</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmojiReaction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import Part from 'src/components/Part'
|
||||
// import Part from 'src/components/Part'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_PART_BY_ID($id: Int!) {
|
||||
@@ -42,5 +42,6 @@ export const Success = ({ part }) => {
|
||||
console.log(id, input, 'wowow')
|
||||
updatePart({ variables: { id, input } })
|
||||
}
|
||||
return <Part part={{...part, code: part.code}} saveCode={saveCode} loading={loading} error={error} />
|
||||
return <div>TODO part</div>
|
||||
// return <Part part={{...part, code: part.code}} saveCode={saveCode} loading={loading} error={error} />
|
||||
}
|
||||
|
||||
@@ -11,19 +11,25 @@ import Svg from 'src/components/Svg/Svg.js'
|
||||
const CLOUDINARY_UPLOAD_PRESET = "CadHub_project_images";
|
||||
const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload";
|
||||
|
||||
export default function ImageUploader({ onImageUpload, imageUrl }) {
|
||||
export default function ImageUploader({
|
||||
onImageUpload,
|
||||
imageUrl,
|
||||
aspectRatio,
|
||||
className,
|
||||
isEditable,
|
||||
width=600
|
||||
}) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [file, setFile] = useState()
|
||||
const [cloudinaryId, setCloudinaryId] = useState(imageUrl)
|
||||
const [imageObj, setImageObj] = useState()
|
||||
const [crop, setCrop] = useState({
|
||||
aspect: 16 / 9,
|
||||
aspect: aspectRatio,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
});
|
||||
async function handleImageUpload() {
|
||||
const croppedFile = await getCroppedImg(imageObj, crop, 'avatar')
|
||||
console.log(croppedFile)
|
||||
const imageData = new FormData();
|
||||
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
|
||||
imageData.append('file', croppedFile);
|
||||
@@ -51,27 +57,29 @@ export default function ImageUploader({ onImageUpload, imageUrl }) {
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||
return (
|
||||
<div className="m-8">
|
||||
<div className="w-full relative" {...getRootProps()}>
|
||||
{cloudinaryId && <button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
|
||||
<Svg name="pencil" strokeWidth={2} className="text-gray-300 h-48 w-48" />
|
||||
<div className={'relative overflow-hidden '+ (!imageUrl && isEditable ? 'border ' : '') + className} style={{paddingBottom: `${1/aspectRatio*100}%`}}>
|
||||
<div className="absolute w-full h-full" {...getRootProps()}>
|
||||
{cloudinaryId && isEditable && <button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
|
||||
<Svg name="pencil" strokeWidth={2} className="text-gray-300 h-24 w-24" />
|
||||
</button>}
|
||||
<input {...getInputProps()} />
|
||||
{cloudinaryId && <div className="relative">
|
||||
{isEditable && <input {...getInputProps()} />}
|
||||
{(cloudinaryId || !isEditable) && <div className="relative overflow-hidden w-full h-full">
|
||||
<CloudinaryImage
|
||||
className="object-cover w-full rounded shadow"
|
||||
className="object-cover w-full h-full shadow overflow-hidden"
|
||||
cloudName="irevdev"
|
||||
publicId={cloudinaryId}
|
||||
width="600"
|
||||
publicId={cloudinaryId || 'CadHub/eia1kwru54g2kf02s2xx'}
|
||||
width={width}
|
||||
crop="scale"
|
||||
/>
|
||||
</div>}
|
||||
{!cloudinaryId && <button className="absolute inset-0"></button>}
|
||||
{!cloudinaryId && <div className="mt-3 text-indigo-500 border-dashed border border-indigo-500 py-8 text-center rounded-lg w-full">
|
||||
{!cloudinaryId && isEditable && <div className="text-indigo-500 flex items-center justify-center rounded-lg w-full h-full">
|
||||
<div className="px-6 text-center">
|
||||
Drop files here ...
|
||||
or <span className="group flex w-full items-center justify-center py-4">
|
||||
or <span className="group flex w-full items-center justify-center py-2">
|
||||
<span className="bg-indigo-500 shadow rounded text-gray-200 cursor-pointer p-2 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-150">upload</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
<Dialog
|
||||
@@ -117,4 +125,3 @@ function getCroppedImg(image, crop, fileName) {
|
||||
}, 'image/jpeg', 1);
|
||||
});
|
||||
}
|
||||
|
||||
42
web/src/components/ImageUploader/ImageUploader.stories.js
Normal file
42
web/src/components/ImageUploader/ImageUploader.stories.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import ImageUploader from './ImageUploader'
|
||||
|
||||
export const generated = () => {
|
||||
return (
|
||||
<>
|
||||
<h3>AspectRatio:1, no initial image, editable</h3>
|
||||
<
|
||||
ImageUploader
|
||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
||||
aspectRatio={1}
|
||||
isEditable={true}
|
||||
className={"bg-red-400 rounded-half rounded-br-xl"}
|
||||
/>
|
||||
<h3>AspectRatio 16:9, no initial image, editable</h3>
|
||||
<
|
||||
ImageUploader
|
||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
||||
aspectRatio={16/9}
|
||||
isEditable={true}
|
||||
className={"bg-red-400 rounded-xl"}
|
||||
imageUrl="CadHub/inakek2urbreynblzhgt"
|
||||
/>
|
||||
<h3>AspectRatio:1, no initial image, NOT editable</h3>
|
||||
<
|
||||
ImageUploader
|
||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
||||
aspectRatio={1}
|
||||
className={"rounded-half rounded-br-xl"}
|
||||
/>
|
||||
<h3>AspectRatio ,16:9 no initial image, NOT editable</h3>
|
||||
<
|
||||
ImageUploader
|
||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
||||
aspectRatio={16/9}
|
||||
className={"rounded-xl"}
|
||||
imageUrl="CadHub/inakek2urbreynblzhgt"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default { title: 'Components/ImageUploader' }
|
||||
11
web/src/components/ImageUploader/ImageUploader.test.js
Normal file
11
web/src/components/ImageUploader/ImageUploader.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import ImageUploader from './ImageUploader'
|
||||
|
||||
describe('ImageUploader', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<ImageUploader />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
21
web/src/components/InputText/InputText.js
Normal file
21
web/src/components/InputText/InputText.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getActiveClasses } from 'get-active-classes'
|
||||
|
||||
const InputText = ({value, isEditable, onChange ,className}) => {
|
||||
return (
|
||||
<>
|
||||
<div className={getActiveClasses('relative inline-block', {'hidden': !isEditable}, className)}>
|
||||
<div className="absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner bg-gray-100" />
|
||||
<input
|
||||
className="pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
readOnly={!onChange}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<span className={getActiveClasses('pl-2 text-indigo-800 font-medium mb-px pb-px',{'hidden': isEditable}, className)}>{value}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputText
|
||||
7
web/src/components/InputText/InputText.stories.js
Normal file
7
web/src/components/InputText/InputText.stories.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import InputText from './InputText'
|
||||
|
||||
export const generated = () => {
|
||||
return <InputText />
|
||||
}
|
||||
|
||||
export default { title: 'Components/InputText' }
|
||||
@@ -1,11 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import AboutPage from './AboutPage'
|
||||
import InputText from './InputText'
|
||||
|
||||
describe('AboutPage', () => {
|
||||
describe('InputText', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<AboutPage />)
|
||||
render(<InputText />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
41
web/src/components/NewComment/NewComment.js
Normal file
41
web/src/components/NewComment/NewComment.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import CommentForm from 'src/components/CommentForm'
|
||||
|
||||
const CREATE_COMMENT_MUTATION = gql`
|
||||
mutation CreateCommentMutation($input: CreateCommentInput!) {
|
||||
createComment(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NewComment = () => {
|
||||
const { addMessage } = useFlash()
|
||||
const [createComment, { loading, error }] = useMutation(
|
||||
CREATE_COMMENT_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
navigate(routes.comments())
|
||||
addMessage('Comment created.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (input) => {
|
||||
createComment({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">New Comment</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<CommentForm onSave={onSave} loading={loading} error={error} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewComment
|
||||
41
web/src/components/NewPartReaction/NewPartReaction.js
Normal file
41
web/src/components/NewPartReaction/NewPartReaction.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PartReactionForm from 'src/components/PartReactionForm'
|
||||
|
||||
const CREATE_PART_REACTION_MUTATION = gql`
|
||||
mutation TogglePartReactionMutation($input: TogglePartReactionInput!) {
|
||||
togglePartReaction(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NewPartReaction = () => {
|
||||
const { addMessage } = useFlash()
|
||||
const [togglePartReaction, { loading, error }] = useMutation(
|
||||
CREATE_PART_REACTION_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
navigate(routes.partReactions())
|
||||
addMessage('PartReaction created.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (input) => {
|
||||
togglePartReaction({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">New PartReaction</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PartReactionForm onSave={onSave} loading={loading} error={error} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPartReaction
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PostForm from 'src/components/PostForm'
|
||||
|
||||
const CREATE_POST_MUTATION = gql`
|
||||
mutation CreatePostMutation($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NewPost = () => {
|
||||
const { addMessage } = useFlash()
|
||||
const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.posts())
|
||||
addMessage('Post created.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onSave = (input) => {
|
||||
createPost({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">New Post</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PostForm onSave={onSave} loading={loading} error={error} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPost
|
||||
@@ -1,23 +1,35 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
import { initialize } from 'src/cascade/js/MainPage/CascadeMain'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const DELETE_PART_MUTATION = gql`
|
||||
mutation DeletePartMutation($id: Int!) {
|
||||
mutation DeletePartMutation($id: String!) {
|
||||
deletePart(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Part = ({ part, saveCode, loading, error}) => {
|
||||
const [code, setCode] = useState(part.code)
|
||||
useEffect(() => {
|
||||
const sickCallback = (code) => setCode(code)
|
||||
initialize(sickCallback, part.code)
|
||||
}, [])
|
||||
const hasChanges = code !== part.code
|
||||
const jsonDisplay = (obj) => {
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(obj, null, 2)}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
const timeTag = (datetime) => {
|
||||
return (
|
||||
<time dateTime={datetime} title={datetime}>
|
||||
{new Date(datetime).toUTCString()}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
||||
const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const Part = ({ part }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
|
||||
onCompleted: () => {
|
||||
@@ -42,6 +54,10 @@ const Part = ({ part, saveCode, loading, error}) => {
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>{part.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{part.title}</td>
|
||||
@@ -50,59 +66,44 @@ const Part = ({ part, saveCode, loading, error}) => {
|
||||
<th>Description</th>
|
||||
<td>{part.description}</td>
|
||||
</tr>
|
||||
{/* <tr>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<td>{part.code}</td>
|
||||
</tr> */}
|
||||
{/* <tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Main image</th>
|
||||
<td>{part.mainImage}</td>
|
||||
</tr> */}
|
||||
{/* <tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
<td>{timeTag(part.createdAt)}</td>
|
||||
</tr> */}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Updated at</th>
|
||||
<td>{timeTag(part.updatedAt)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>User id</th>
|
||||
<td>{part.userId}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<img src={part.mainImage} />
|
||||
</div>
|
||||
<nav className="rw-button-group">
|
||||
{loading && 'Loading...'}
|
||||
{hasChanges && !loading && <button onClick={() => saveCode({code}, part.id)} className="rw-button rw-button-blue">
|
||||
Save Changes
|
||||
</button>}
|
||||
<Link
|
||||
to={routes.editPart({ id: part.id })}
|
||||
className="rw-button rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<a
|
||||
href="#"
|
||||
className="rw-button rw-button-red"
|
||||
onClick={() => onDeleteClick(part.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</nav>
|
||||
<div>
|
||||
<div id="topnav" className="topnav">
|
||||
<a href="https://github.com/zalo/CascadeStudio">Cascade Studio 0.0.6</a>
|
||||
<a href="#" id="main-proj-button" title="Sets this project to save in local storage." onClick={() => makeMainProject()}>Make Main Project</a>
|
||||
<a href="#" title="Save Project to .json" onClick={() => saveProject()}>Save Project</a>
|
||||
<label htmlFor="project-file" title="Load Project from .json">Load Project
|
||||
<input
|
||||
id="project-file"
|
||||
name="project-file"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style={{display:'none'}}
|
||||
onInput={() => loadProject()}
|
||||
/>
|
||||
</label>
|
||||
<a href="#" onClick={() => threejsViewport.saveShapeSTEP()}>Save STEP</a>
|
||||
<a href="#" onClick={() => threejsViewport.saveShapeSTL()}>Save STL</a>
|
||||
<a href="#" onClick={() => threejsViewport.saveShapeOBJ()}>Save OBJ</a>
|
||||
<label htmlFor="files" title="Import STEP, IGES, or (ASCII) STL from File">Import STEP/IGES/STL
|
||||
<input id="files" name="files" type="file" accept=".iges,.step,.igs,.stp,.stl" multiple style={{display: 'none'}} onInput={ () =>loadFiles()}/>
|
||||
</label>
|
||||
<a href="#" title="Clears the external step/iges/stl files stored in the project." onClick={() => clearExternalFiles()}>Clear Imported Files</a>
|
||||
<a href="" title="Resets the project and localstorage." onClick={() => {
|
||||
window.localStorage.clear();
|
||||
window.history.replaceState({}, 'Cascade Studio','?')
|
||||
}}>Reset Project</a>
|
||||
</div>
|
||||
<div id="cascade-container" style={{height:'auto'}}>
|
||||
</div>
|
||||
<footer>footer</footer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
140
web/src/components/Part2Cell/Part2Cell.js
Normal file
140
web/src/components/Part2Cell/Part2Cell.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
|
||||
import PartProfile from 'src/components/PartProfile'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_PART_BY_USERNAME_TITLE($userName: String!, $partTitle: String, $currentUserId: String) {
|
||||
userPart: userName(userName: $userName) {
|
||||
id
|
||||
name
|
||||
userName
|
||||
bio
|
||||
image
|
||||
Part(partTitle: $partTitle) {
|
||||
id
|
||||
title
|
||||
description
|
||||
code
|
||||
mainImage
|
||||
createdAt
|
||||
updatedAt
|
||||
userId
|
||||
Reaction {
|
||||
emote
|
||||
}
|
||||
userReactions: Reaction(userId: $currentUserId) {
|
||||
emote
|
||||
}
|
||||
Comment {
|
||||
id
|
||||
text
|
||||
user {
|
||||
userName
|
||||
image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UPDATE_PART_MUTATION = gql`
|
||||
mutation UpdatePartMutation($id: String!, $input: UpdatePartInput!) {
|
||||
updatePart:updatePart(id: $id, input: $input) {
|
||||
id
|
||||
title
|
||||
user {
|
||||
id
|
||||
userName
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const CREATE_PART_MUTATION = gql`
|
||||
mutation CreatePartMutation($input: CreatePartInput!) {
|
||||
createPart(input: $input) {
|
||||
id
|
||||
title
|
||||
user {
|
||||
id
|
||||
userName
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const TOGGLE_REACTION_MUTATION = gql`
|
||||
mutation ToggleReactionMutation($input: TogglePartReactionInput!) {
|
||||
togglePartReaction(input: $input){
|
||||
id
|
||||
emote
|
||||
}
|
||||
}
|
||||
`
|
||||
const CREATE_COMMENT_MUTATION = gql`
|
||||
mutation CreateCommentMutation($input: CreateCommentInput!) {
|
||||
createComment(input: $input) {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
|
||||
export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
|
||||
const { currentUser } = useAuth()
|
||||
const { addMessage } = useFlash()
|
||||
const [updateUser, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
||||
onCompleted: ({updatePart}) => {
|
||||
navigate(routes.part2({userName: updatePart.user.userName, partTitle: updatePart.title}))
|
||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
const [createUser] = useMutation(CREATE_PART_MUTATION, {
|
||||
onCompleted: ({createPart}) => {
|
||||
navigate(routes.part2({userName: createPart?.user?.userName, partTitle: createPart?.title}))
|
||||
addMessage('Part Created.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
const onSave = (id, input) => {
|
||||
if(!id) {
|
||||
createUser({ variables: { input } })
|
||||
return
|
||||
}
|
||||
updateUser({ variables: { id, input } })
|
||||
}
|
||||
|
||||
const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, {
|
||||
onCompleted: () => refetch()
|
||||
})
|
||||
const onReaction = (emote) => toggleReaction({variables: {input: {
|
||||
emote,
|
||||
userId: currentUser.sub,
|
||||
partId: userPart?.Part?.id,
|
||||
}}})
|
||||
|
||||
const [createComment] = useMutation(CREATE_COMMENT_MUTATION, {
|
||||
onCompleted: () => refetch()
|
||||
})
|
||||
const onComment = (text) => createComment({variables: {input: {
|
||||
text,
|
||||
userId: currentUser.sub,
|
||||
partId: userPart?.Part?.id,
|
||||
}}})
|
||||
|
||||
return <PartProfile
|
||||
userPart={userPart}
|
||||
onSave={onSave}
|
||||
loading={loading}
|
||||
error={error}
|
||||
isEditable={isEditable}
|
||||
onReaction={onReaction}
|
||||
onComment={onComment}
|
||||
/>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
blogPost: {
|
||||
part2: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Loading, Empty, Failure, Success } from './BlogPostCell'
|
||||
import { standard } from './BlogPostCell.mock'
|
||||
import { Loading, Empty, Failure, Success } from './Part2Cell'
|
||||
import { standard } from './Part2Cell.mock'
|
||||
|
||||
export const loading = () => {
|
||||
return Loading ? <Loading /> : null
|
||||
@@ -17,4 +17,4 @@ export const success = () => {
|
||||
return Success ? <Success {...standard()} /> : null
|
||||
}
|
||||
|
||||
export default { title: 'Cells/BlogPostCell' }
|
||||
export default { title: 'Cells/Part2Cell' }
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from '@redwoodjs/testing'
|
||||
import { Loading, Empty, Failure, Success } from './BlogPostCell'
|
||||
import { standard } from './BlogPostCell.mock'
|
||||
import { Loading, Empty, Failure, Success } from './Part2Cell'
|
||||
import { standard } from './Part2Cell.mock'
|
||||
|
||||
describe('BlogPostCell', () => {
|
||||
describe('Part2Cell', () => {
|
||||
test('Loading renders successfully', () => {
|
||||
render(<Loading />)
|
||||
// Use screen.debug() to see output
|
||||
@@ -20,7 +20,7 @@ describe('BlogPostCell', () => {
|
||||
})
|
||||
|
||||
test('Success renders successfully', async () => {
|
||||
render(<Success blogPost={standard().blogPost} />)
|
||||
render(<Success part2={standard().part2} />)
|
||||
expect(screen.getByText(/42/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,32 +1,24 @@
|
||||
import Part from 'src/components/Part'
|
||||
|
||||
import {QUERY as reExportQuery} from 'src/components/EditPartCell'
|
||||
import Editor from "rich-markdown-editor";
|
||||
|
||||
export const QUERY = reExportQuery
|
||||
export const QUERY = gql`
|
||||
query FIND_PART_BY_ID($id: String!) {
|
||||
part: part(id: $id) {
|
||||
id
|
||||
title
|
||||
description
|
||||
code
|
||||
mainImage
|
||||
createdAt
|
||||
updatedAt
|
||||
userId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
export const Empty = () => <div>Part not found</div>
|
||||
|
||||
export const Success = ({ part }) => {
|
||||
console.log(part)
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex justify-center">
|
||||
<img src={part.mainImage} />
|
||||
</div>
|
||||
<div className="bg-white p-8 my-12 min-h-md">
|
||||
<h2 className="text-4xl py-4">{part.title}</h2>
|
||||
<Editor
|
||||
className="markdown-overrides"
|
||||
defaultValue={part.description}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return <Part part={part} />
|
||||
}
|
||||
|
||||
@@ -6,36 +6,14 @@ import {
|
||||
TextField,
|
||||
Submit,
|
||||
} from '@redwoodjs/forms'
|
||||
import { useState } from 'react';
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import { useFlash } from '@redwoodjs/web'
|
||||
import ImageUploader from './ImageUploader.js'
|
||||
|
||||
|
||||
import Editor from "rich-markdown-editor";
|
||||
|
||||
const PartForm = (props) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [description, setDescription] = useState(props?.part?.description)
|
||||
const [imageUrl, setImageUrl] = useState(props?.part?.mainImage)
|
||||
const onSubmit = async (data, e) => {
|
||||
|
||||
await props.onSave({
|
||||
...data,
|
||||
description,
|
||||
mainImage: imageUrl
|
||||
}, props?.part?.id)
|
||||
const shouldOpenIde = e?.nativeEvent?.submitter?.dataset?.openIde
|
||||
if(shouldOpenIde) {
|
||||
navigate(routes.partIde({id: props?.part?.id}))
|
||||
} else {
|
||||
navigate(routes.part({id: props?.part?.id}))
|
||||
}
|
||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||
const onSubmit = (data) => {
|
||||
props.onSave(data, props?.part?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto mt-10">
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} error={props.error}>
|
||||
<FormError
|
||||
error={props.error}
|
||||
@@ -46,7 +24,7 @@ const PartForm = (props) => {
|
||||
|
||||
<Label
|
||||
name="title"
|
||||
className="p-0"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Title
|
||||
@@ -60,30 +38,74 @@ const PartForm = (props) => {
|
||||
/>
|
||||
<FieldError name="title" className="rw-field-error" />
|
||||
|
||||
<ImageUploader onImageUpload={({cloudinaryPublicId}) => setImageUrl(cloudinaryPublicId)} />
|
||||
|
||||
|
||||
<Label
|
||||
name="description"
|
||||
className="p-0"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Description
|
||||
</Label>
|
||||
<div name="description" className="markdown-overrides bg-white p-12 my-10 min-h-md">
|
||||
<Editor
|
||||
defaultValue={props.part?.description}
|
||||
onChange={(valueFn) => setDescription(valueFn())}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
name="description"
|
||||
defaultValue={props.part?.description}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="description" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="code"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Code
|
||||
</Label>
|
||||
<TextField
|
||||
name="code"
|
||||
defaultValue={props.part?.code}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="code" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="mainImage"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Main image
|
||||
</Label>
|
||||
<TextField
|
||||
name="mainImage"
|
||||
defaultValue={props.part?.mainImage}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="mainImage" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="userId"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
User id
|
||||
</Label>
|
||||
<TextField
|
||||
name="userId"
|
||||
defaultValue={props.part?.userId}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="userId" className="rw-field-error" />
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||
Save
|
||||
</Submit>
|
||||
<Submit disabled={props.loading} data-open-ide={true} className="rw-button rw-button-blue">
|
||||
Save and open IDE
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
173
web/src/components/PartProfile/PartProfile.js
Normal file
173
web/src/components/PartProfile/PartProfile.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||
import Editor from "rich-markdown-editor";
|
||||
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import Breadcrumb from 'src/components/Breadcrumb'
|
||||
import EmojiReaction from 'src/components/EmojiReaction'
|
||||
import Button from 'src/components/Button'
|
||||
import { countEmotes } from 'src/helpers/emote'
|
||||
import { getActiveClasses } from 'get-active-classes';
|
||||
|
||||
const PartProfile = ({
|
||||
userPart,
|
||||
isEditable,
|
||||
onSave,
|
||||
loading,
|
||||
error,
|
||||
onReaction,
|
||||
onComment,
|
||||
}) => {
|
||||
const [comment, setComment] = useState('')
|
||||
const { currentUser } = useAuth()
|
||||
const canEdit = currentUser?.sub === userPart.id
|
||||
const part = userPart?.Part
|
||||
const emotes = countEmotes(part?.Reaction)
|
||||
const userEmotes = part?.userReactions.map(({emote}) => emote)
|
||||
useEffect(() => {isEditable &&
|
||||
!canEdit &&
|
||||
navigate(routes.part2({userName: userPart.userName, partTitle: part.title}))},
|
||||
[currentUser])
|
||||
const [input, setInput] = useState({
|
||||
title: part?.title,
|
||||
mainImage: part?.mainImage,
|
||||
description: part?.description,
|
||||
userId: userPart?.id,
|
||||
})
|
||||
const setProperty = (property, value) => setInput({
|
||||
...input,
|
||||
[property]: value,
|
||||
})
|
||||
const onTitleChange = ({target}) => setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
|
||||
const onDescriptionChange = (description) => setProperty('description', description())
|
||||
const onImageUpload = ({cloudinaryPublicId}) => setProperty('mainImage', cloudinaryPublicId)
|
||||
const onEditSaveClick = () => {
|
||||
if (isEditable) {
|
||||
input.title && onSave(part?.id, input)
|
||||
return
|
||||
}
|
||||
navigate(routes.editPart2({userName: userPart.userName, partTitle: part.title}))
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="grid mt-20 gap-8" style={{gridTemplateColumns: 'auto 12rem minmax(12rem, 42rem) auto'}}>
|
||||
|
||||
{/* Side column */}
|
||||
<aside className="col-start-2 relative">
|
||||
<ImageUploader
|
||||
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
|
||||
onImageUpload={() => {}}
|
||||
aspectRatio={1}
|
||||
imageUrl={userPart.image}
|
||||
width={300}
|
||||
/>
|
||||
<h4 className="text-indigo-800 text-xl underline text-right py-4"><Link to={routes.user2({userName: userPart.userName})}>{userPart?.name}</Link></h4>
|
||||
<div className="h-px bg-indigo-200 mb-4" />
|
||||
<EmojiReaction
|
||||
emotes={emotes}
|
||||
userEmotes={userEmotes}
|
||||
onEmote={onReaction}
|
||||
/>
|
||||
<Button
|
||||
className="mt-6 ml-auto hover:shadow-lg bg-gradient-to-r from-transparent to-indigo-100"
|
||||
shouldAnimateHover
|
||||
iconName="chevron-down"
|
||||
onClick={() => {}}
|
||||
>
|
||||
Comments 11
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200"
|
||||
shouldAnimateHover
|
||||
iconName="terminal"
|
||||
onClick={() => {}}
|
||||
>
|
||||
Open IDE
|
||||
</Button>
|
||||
{canEdit && <Button
|
||||
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
|
||||
shouldAnimateHover
|
||||
iconName={isEditable ? 'save' : 'pencil'}
|
||||
onClick={onEditSaveClick}
|
||||
>
|
||||
{isEditable ? 'Save Details' : 'Edit Details'}
|
||||
</Button>}
|
||||
{isEditable && <div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />}
|
||||
</aside>
|
||||
|
||||
{/* main project center column */}
|
||||
<section className="col-start-3">
|
||||
<Breadcrumb className="inline" onPartTitleChange={isEditable && onTitleChange} userName={userPart.userName} partTitle={input?.title}/>
|
||||
{ !!(input?.mainImage || isEditable) && <ImageUploader
|
||||
className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8"
|
||||
onImageUpload={onImageUpload}
|
||||
aspectRatio={16/9}
|
||||
isEditable={isEditable}
|
||||
imageUrl={input?.mainImage}
|
||||
width={1010}
|
||||
/>}
|
||||
<div name="description" className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-8 min-h-md">
|
||||
<Editor
|
||||
defaultValue={part?.description || ''}
|
||||
readOnly={!isEditable}
|
||||
onChange={onDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* comments */}
|
||||
{ !isEditable && <>
|
||||
<div className="h-px bg-indigo-200 mt-8" />
|
||||
<h3 className="text-indigo-800 text-lg font-roboto tracking-wider mb-4" >Comments</h3>
|
||||
|
||||
<ul>
|
||||
{part?.Comment.map(({text, user, id}) => (
|
||||
<li key={id} className="flex mb-6">
|
||||
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow flex-shrink-0">
|
||||
<ImageUploader
|
||||
className=""
|
||||
onImageUpload={() => {}}
|
||||
aspectRatio={1}
|
||||
imageUrl={user?.image}
|
||||
width={50}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4 font-roboto">
|
||||
<div className="text-gray-800 font-bold text-lg mb-1"><Link to={routes.user2({userName: user.userName})}>
|
||||
{user.userName}
|
||||
</Link></div>
|
||||
<div className="text-gray-700 p-3 rounded bg-gray-200 shadow">
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{currentUser && <>
|
||||
<div className="mt-12 ml-12">
|
||||
<textarea
|
||||
className="w-full h-32 rounded-lg shadow-inner outline-none resize-none p-3"
|
||||
placeholder="Leave a comment"
|
||||
value={comment}
|
||||
onChange={({target}) => setComment(target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={getActiveClasses("ml-auto hover:shadow-lg bg-gray-300 mt-4 mb-20", {'bg-indigo-200': currentUser})}
|
||||
shouldAnimateHover
|
||||
disabled={!currentUser}
|
||||
iconName={'save'}
|
||||
onClick={() => onComment(comment)}
|
||||
>Comment</Button>
|
||||
</>}
|
||||
</>}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartProfile
|
||||
7
web/src/components/PartProfile/PartProfile.stories.js
Normal file
7
web/src/components/PartProfile/PartProfile.stories.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import PartProfile from './PartProfile'
|
||||
|
||||
export const generated = () => {
|
||||
return <PartProfile />
|
||||
}
|
||||
|
||||
export default { title: 'Components/PartProfile' }
|
||||
@@ -1,11 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import ContactPage from './ContactPage'
|
||||
import PartProfile from './PartProfile'
|
||||
|
||||
describe('ContactPage', () => {
|
||||
describe('PartProfile', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<ContactPage />)
|
||||
render(<PartProfile />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
103
web/src/components/PartReaction/PartReaction.js
Normal file
103
web/src/components/PartReaction/PartReaction.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
|
||||
const DELETE_PART_REACTION_MUTATION = gql`
|
||||
mutation DeletePartReactionMutation($id: String!) {
|
||||
deletePartReaction(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const jsonDisplay = (obj) => {
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(obj, null, 2)}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
const timeTag = (datetime) => {
|
||||
return (
|
||||
<time dateTime={datetime} title={datetime}>
|
||||
{new Date(datetime).toUTCString()}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
||||
const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const PartReaction = ({ partReaction }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePartReaction] = useMutation(DELETE_PART_REACTION_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.partReactions())
|
||||
addMessage('PartReaction deleted.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id) => {
|
||||
if (confirm('Are you sure you want to delete partReaction ' + id + '?')) {
|
||||
deletePartReaction({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
PartReaction {partReaction.id} Detail
|
||||
</h2>
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>{partReaction.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Emote</th>
|
||||
<td>{partReaction.emote}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>User id</th>
|
||||
<td>{partReaction.userId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Part id</th>
|
||||
<td>{partReaction.partId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
<td>{timeTag(partReaction.createdAt)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Updated at</th>
|
||||
<td>{timeTag(partReaction.updatedAt)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav className="rw-button-group">
|
||||
<Link
|
||||
to={routes.editPartReaction({ id: partReaction.id })}
|
||||
className="rw-button rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<a
|
||||
href="#"
|
||||
className="rw-button rw-button-red"
|
||||
onClick={() => onDeleteClick(partReaction.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartReaction
|
||||
22
web/src/components/PartReactionCell/PartReactionCell.js
Normal file
22
web/src/components/PartReactionCell/PartReactionCell.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import PartReaction from 'src/components/PartReaction'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_PART_REACTION_BY_ID($id: String!) {
|
||||
partReaction: partReaction(id: $id) {
|
||||
id
|
||||
emote
|
||||
userId
|
||||
partId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>PartReaction not found</div>
|
||||
|
||||
export const Success = ({ partReaction }) => {
|
||||
return <PartReaction partReaction={partReaction} />
|
||||
}
|
||||
83
web/src/components/PartReactionForm/PartReactionForm.js
Normal file
83
web/src/components/PartReactionForm/PartReactionForm.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Form,
|
||||
FormError,
|
||||
FieldError,
|
||||
Label,
|
||||
TextField,
|
||||
Submit,
|
||||
} from '@redwoodjs/forms'
|
||||
|
||||
const PartReactionForm = (props) => {
|
||||
const onSubmit = (data) => {
|
||||
props.onSave(data, props?.partReaction?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} error={props.error}>
|
||||
<FormError
|
||||
error={props.error}
|
||||
wrapperClassName="rw-form-error-wrapper"
|
||||
titleClassName="rw-form-error-title"
|
||||
listClassName="rw-form-error-list"
|
||||
/>
|
||||
|
||||
<Label
|
||||
name="emote"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Emote
|
||||
</Label>
|
||||
<TextField
|
||||
name="emote"
|
||||
defaultValue={props.partReaction?.emote}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="emote" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="userId"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
User id
|
||||
</Label>
|
||||
<TextField
|
||||
name="userId"
|
||||
defaultValue={props.partReaction?.userId}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="userId" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="partId"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Part id
|
||||
</Label>
|
||||
<TextField
|
||||
name="partId"
|
||||
defaultValue={props.partReaction?.partId}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="partId" className="rw-field-error" />
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||
Save
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartReactionForm
|
||||
112
web/src/components/PartReactions/PartReactions.js
Normal file
112
web/src/components/PartReactions/PartReactions.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
const DELETE_PART_REACTION_MUTATION = gql`
|
||||
mutation DeletePartReactionMutation($id: String!) {
|
||||
deletePartReaction(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MAX_STRING_LENGTH = 150
|
||||
|
||||
const truncate = (text) => {
|
||||
let output = text
|
||||
if (text && text.length > MAX_STRING_LENGTH) {
|
||||
output = output.substring(0, MAX_STRING_LENGTH) + '...'
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const jsonTruncate = (obj) => {
|
||||
return truncate(JSON.stringify(obj, null, 2))
|
||||
}
|
||||
|
||||
const timeTag = (datetime) => {
|
||||
return (
|
||||
<time dateTime={datetime} title={datetime}>
|
||||
{new Date(datetime).toUTCString()}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
||||
const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const PartReactionsList = ({ partReactions }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePartReaction] = useMutation(DELETE_PART_REACTION_MUTATION, {
|
||||
onCompleted: () => {
|
||||
addMessage('PartReaction deleted.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id) => {
|
||||
if (confirm('Are you sure you want to delete partReaction ' + id + '?')) {
|
||||
deletePartReaction({
|
||||
variables: { id },
|
||||
refetchQueries: ['PART_REACTIONS'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment rw-table-wrapper-responsive">
|
||||
<table className="rw-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Emote</th>
|
||||
<th>User id</th>
|
||||
<th>Part id</th>
|
||||
<th>Created at</th>
|
||||
<th>Updated at</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{partReactions.map((partReaction) => (
|
||||
<tr key={partReaction.id}>
|
||||
<td>{truncate(partReaction.id)}</td>
|
||||
<td>{truncate(partReaction.emote)}</td>
|
||||
<td>{truncate(partReaction.userId)}</td>
|
||||
<td>{truncate(partReaction.partId)}</td>
|
||||
<td>{timeTag(partReaction.createdAt)}</td>
|
||||
<td>{timeTag(partReaction.updatedAt)}</td>
|
||||
<td>
|
||||
<nav className="rw-table-actions">
|
||||
<Link
|
||||
to={routes.partReaction({ id: partReaction.id })}
|
||||
title={'Show partReaction ' + partReaction.id + ' detail'}
|
||||
className="rw-button rw-button-small"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editPartReaction({ id: partReaction.id })}
|
||||
title={'Edit partReaction ' + partReaction.id}
|
||||
className="rw-button rw-button-small rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<a
|
||||
href="#"
|
||||
title={'Delete partReaction ' + partReaction.id}
|
||||
className="rw-button rw-button-small rw-button-red"
|
||||
onClick={() => onDeleteClick(partReaction.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</nav>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartReactionsList
|
||||
33
web/src/components/PartReactionsCell/PartReactionsCell.js
Normal file
33
web/src/components/PartReactionsCell/PartReactionsCell.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
import PartReactions from 'src/components/PartReactions'
|
||||
|
||||
export const QUERY = gql`
|
||||
query PART_REACTIONS {
|
||||
partReactions {
|
||||
id
|
||||
emote
|
||||
userId
|
||||
partId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => {
|
||||
return (
|
||||
<div className="rw-text-center">
|
||||
{'No partReactions yet. '}
|
||||
<Link to={routes.newPartReaction()} className="rw-link">
|
||||
{'Create one?'}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Success = ({ partReactions }) => {
|
||||
return <PartReactions partReactions={partReactions} />
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user