From b403930695650891dac11aab4bbc68f8bfaa0f9b Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Mon, 9 Nov 2020 19:06:31 +1100 Subject: [PATCH 01/22] Add netlify badge to readme and link to website prominently --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb4248b..6970be4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# CadHub +# [CadHub](https://cadhub.xyz) + +[![Netlify Status](https://api.netlify.com/api/v1/badges/77f37543-e54a-4723-8136-157c0221ec27/deploy-status)](https://app.netlify.com/sites/cadhubxyz/deploys) CadHub aims to be a community website for javascript based code-cad. Currently trying to integrate [cascadeStudio](https://zalo.github.io/CascadeStudio/), but if successful plan to also integrate [JSCAD](https://openjscad.org/). OpenScad has proven code-cad a much loved formate for cad-modeling. Joining code-cad to a mature language like javascript that has a package manager (npm) plus a community hub for sharing cad models like CadHub, we're going to build a thriving community. -- 2.39.5 From 31be14e8b4adae270f761a870a5f26b8124a1806 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Mon, 9 Nov 2020 20:43:59 +1100 Subject: [PATCH 02/22] Add seed data to seed.js good for getting others up to speed --- README.md | 9 +++++ api/prisma/seeds.js | 92 +++++++++++++++++++++++++++++++++++++++++++++ api/src/lib/auth.js | 4 ++ 3 files changed, 105 insertions(+) diff --git a/README.md b/README.md index 6970be4..8061bdc 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ yarn install Initialise the db ``` terminal yarn rw db up +yarn rw db seed ``` ### Fire up dev @@ -36,6 +37,14 @@ yarn rw dev Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`. +you can sign in to the following accounts locally + +localUser1@kurthutten.com: `abc123` + +localUser2@kurthutten.com: `abc123` + +localAdmin@kurthutten.com: `abc123` + You may need to register a account depending on what issue you are trying to tackle, This can be done by clicking the login button on the top right. This will open up netlify's idenitiy modal asking for the websites url, since it will notice you developing locally. Enter `https://cadhub.xyz/` than use you email, verify your email and you should be set. (some routes are protected, but permissions is a big area that needs a lot of work in the near future, so it's in a very incomplete state atm) diff --git a/api/prisma/seeds.js b/api/prisma/seeds.js index 51426c5..31d3730 100644 --- a/api/prisma/seeds.js +++ b/api/prisma/seeds.js @@ -15,6 +15,98 @@ async function main() { // if (!existing.length) { // await db.user.create({ data: { name: 'Admin', email: 'admin@email.com' }}) // } + const users = [ + { + id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60", + userName: "local-user-1", + name: "local 1", + email: "localUser1@kurthutten.com" + }, + { + id: "682ba807-d10e-4caf-bf28-74054e46c9ec", + userName: "local-user-2", + name: "local 2", + email: "localUser2@kurthutten.com" + }, + { + id: "5cea3906-1e8e-4673-8f0d-89e6a963c096", + userName: "local-admin-2", + name: "local admin", + email: "localAdmin@kurthutten.com" + }, + ] + + let existing + existing = await db.user.findMany({ where: { id: users[0].id }}) + if(!existing.length) { + await db.user.create({ + data: users[0], + }) + } + existing = await db.user.findMany({ where: { id: users[1].id }}) + if(!existing.length) { + await db.user.create({ + data: users[1], + }) + } + + const parts = [ + { + title: 'demo-part1', + description: '# can be markdown', + mainImage: 'CadHub/kjdlgjnu0xmwksia7xox', + user: { + connect: { + id: users[0].id, + }, + }, + }, + { + title: 'demo-part2', + description: '## [hey](www.google.com)', + user: { + connect: { + id: users[1].id, + }, + }, + }, + ] + + existing = await db.part.findMany({where: { title: parts[0].title}}) + if(!existing.length) { + await db.part.create({ + data: parts[0], + }) + } + existing = await db.part.findMany({where: { title: parts[1].title}}) + if(!existing.length) { + await db.part.create({ + data: parts[1], + }) + } + + + + const aPart = await db.part.findOne({where: { + title_userId: { + title: parts[0].title, + userId: users[0].id, + } + }}) + await db.comment.create({ + data: { + text: "nice part, I like it", + user: {connect: { id: users[0].id}}, + part: {connect: { id: aPart.id}}, + } + }) + await db.partReaction.create({ + data: { + emote: "❤️", + user: {connect: { id: users[0].id}}, + part: {connect: { id: aPart.id}}, + } + }) console.info('No data to seed. See api/prisma/seeds.js for info.') } diff --git a/api/src/lib/auth.js b/api/src/lib/auth.js index 402f218..3889bbc 100644 --- a/api/src/lib/auth.js +++ b/api/src/lib/auth.js @@ -141,4 +141,8 @@ export const requireAuth = ({ role } = {}) => { ) { throw new ForbiddenError("You don't have access to do that.") } + + if(context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') { + throw new ForbiddenError("That's a local admin ONLY.") + } } -- 2.39.5 From 01f1a028374d1659eaf9c18d0b68e6d4e5110549 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Tue, 10 Nov 2020 20:49:19 +1100 Subject: [PATCH 03/22] Add alpha numeric regex to username on sign up resolves #82 --- api/src/functions/identity-signup.js | 3 ++- api/src/services/helpers.js | 2 ++ api/src/services/parts/parts.js | 5 ++--- api/src/services/users/users.js | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/api/src/functions/identity-signup.js b/api/src/functions/identity-signup.js index 1d0a003..b9f3460 100644 --- a/api/src/functions/identity-signup.js +++ b/api/src/functions/identity-signup.js @@ -1,5 +1,6 @@ import { createUserInsecure } from 'src/services/users/users.js' import { db } from 'src/lib/db' +import { enforceAlphaNumeric } from 'src/services/helpers' export const handler = async (req, _context) => { const body = JSON.parse(req.body) @@ -74,7 +75,7 @@ export const handler = async (req, _context) => { const newSeed = count === 1 ? `${seed}_${count}` : seed.slice(0,-1) + count return generateUniqueUserName(newSeed, count) } - const userNameSeed = email.split('@')[0] + const userNameSeed = enforceAlphaNumeric(email.split('@')[0]) const userName = await generateUniqueUserName(userNameSeed) // TODO maybe come up with a better default userName? const input = { email, diff --git a/api/src/services/helpers.js b/api/src/services/helpers.js index ed474f3..d6e21f3 100644 --- a/api/src/services/helpers.js +++ b/api/src/services/helpers.js @@ -11,3 +11,5 @@ export const foreignKeyReplacement = (input) => { }) return output } + +export const enforceAlphaNumeric = (string) => string.replace(/([^a-zA-Z\d_:])/g, '-') diff --git a/api/src/services/parts/parts.js b/api/src/services/parts/parts.js index 1b96b42..260442c 100644 --- a/api/src/services/parts/parts.js +++ b/api/src/services/parts/parts.js @@ -1,8 +1,7 @@ import { db } from 'src/lib/db' -import { foreignKeyReplacement } from 'src/services/helpers' +import { foreignKeyReplacement, enforceAlphaNumeric } 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() @@ -40,7 +39,7 @@ export const updatePart = async ({ id, input }) => { requireAuth() await requireOwnership({partId: id}) if(input.title) { - input.title = input.title.replace(/([^a-zA-Z\d_:])/g, '-') + input.title = enforceAlphaNumeric(input.title) } return db.part.update({ data: foreignKeyReplacement(input), diff --git a/api/src/services/users/users.js b/api/src/services/users/users.js index cae1557..c06e1bf 100644 --- a/api/src/services/users/users.js +++ b/api/src/services/users/users.js @@ -2,6 +2,7 @@ import { db } from 'src/lib/db' import { requireAuth } from 'src/lib/auth' import { requireOwnership } from 'src/lib/owner' import { UserInputError } from '@redwoodjs/api' +import { enforceAlphaNumeric } from 'src/services/helpers' export const users = () => { requireAuth({ role: 'admin' }) @@ -42,7 +43,7 @@ export const updateUserByUserName = async ({ userName, input }) => { requireAuth() await requireOwnership({userName}) if(input.userName) { - input.userName = input.userName.replace(/([^a-zA-Z\d_:])/g, '-') + input.userName = enforceAlphaNumeric(input.userName) } 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 `) -- 2.39.5 From 96c210496dbbc2abda6f36d6b930f82ff3ef890d Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Tue, 10 Nov 2020 20:59:52 +1100 Subject: [PATCH 04/22] Make emoji menu horizontal resolves #88 --- web/src/components/EmojiReaction/EmojiReaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/EmojiReaction/EmojiReaction.js b/web/src/components/EmojiReaction/EmojiReaction.js index 7f7fbf5..f4dd910 100644 --- a/web/src/components/EmojiReaction/EmojiReaction.js +++ b/web/src/components/EmojiReaction/EmojiReaction.js @@ -89,7 +89,7 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => horizontal: 'left', }} > -
+
{emojiMenu.map((emoji, i) => ( - { - isAuthenticated ? -
  • - - {!loading && {}} - aspectRatio={1} - imageUrl={data?.user?.image} - width={80} - />} - -
  • : -
  • - Sign in/up -
  • - } +
  • +
    + {!loading && {}} + aspectRatio={1} + imageUrl={data?.user?.image} + width={80} + />} +
    +
  • + + { + isAuthenticated && currentUser? +
    + +

    Hello {data?.user?.name}

    + +
    +
    + +
    Edit Profile
    + + Logout +
    : + + } +
    -- 2.39.5 From 198c40132fee6793130fabd071bb52d89580711f Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Tue, 10 Nov 2020 20:37:04 +1100 Subject: [PATCH 06/22] Fix new button, also redirects if user is not signed in --- web/src/layouts/MainLayout/MainLayout.js | 9 ++++++--- web/src/pages/NewPart2Page/NewPart2Page.js | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/src/layouts/MainLayout/MainLayout.js b/web/src/layouts/MainLayout/MainLayout.js index 33a8215..1f9fbf7 100644 --- a/web/src/layouts/MainLayout/MainLayout.js +++ b/web/src/layouts/MainLayout/MainLayout.js @@ -72,9 +72,12 @@ const MainLayout = ({ children}) => {
    • - + {isAuthenticated && data?.user?.userName ? + + + : + + }
    • diff --git a/web/src/pages/NewPart2Page/NewPart2Page.js b/web/src/pages/NewPart2Page/NewPart2Page.js index bd316a9..c1cb4e4 100644 --- a/web/src/pages/NewPart2Page/NewPart2Page.js +++ b/web/src/pages/NewPart2Page/NewPart2Page.js @@ -1,11 +1,16 @@ +import { useEffect } from 'react' import { useAuth } from '@redwoodjs/auth' +import { navigate, routes } from '@redwoodjs/router' import MainLayout from 'src/layouts/MainLayout' import Part2Cell from 'src/components/Part2Cell' const NewPart2Page = ({userName}) => { - const { currentUser } = useAuth() + const { isAuthenticated, currentUser } = useAuth() + useEffect(() => {!isAuthenticated && + navigate(routes.home())}, + [currentUser]) return ( Date: Tue, 10 Nov 2020 22:22:45 +0530 Subject: [PATCH 07/22] Fixed PropOver --- web/src/layouts/MainLayout/MainLayout.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/layouts/MainLayout/MainLayout.js b/web/src/layouts/MainLayout/MainLayout.js index 33a8215..045609d 100644 --- a/web/src/layouts/MainLayout/MainLayout.js +++ b/web/src/layouts/MainLayout/MainLayout.js @@ -25,7 +25,6 @@ import logo from 'src/layouts/MainLayout/Logo_2.jpg' const MainLayout = ({ children}) => { const { logIn, logOut, isAuthenticated, currentUser } = useAuth() const {data, loading} = useQuery(QUERY, {variables: {id: currentUser?.sub}}) - console.log(data?.user?.name); const [isOpen, setIsOpen] = useState(false) const [anchorEl, setAnchorEl] = useState(null) const [popoverId, setPopoverId] = useState(undefined) @@ -72,9 +71,12 @@ const MainLayout = ({ children}) => {
    • - + {isAuthenticated && data?.user?.userName ? + + + : + + }
    • @@ -95,11 +97,11 @@ const MainLayout = ({ children}) => { onClose={closePopover} anchorOrigin={{ vertical: 'bottom', - horizontal: 'left', + horizontal: 'right', }} transformOrigin={{ vertical: 'top', - horizontal: 'left', + horizontal: 'right', }} > { -- 2.39.5 From 39898270df26c68be90beb4095c10c1142282093 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Wed, 11 Nov 2020 03:18:10 +1100 Subject: [PATCH 08/22] Lint project --- api/src/functions/identity-signup.js | 7 +- api/src/graphql/parts.sdl.js | 2 +- api/src/lib/auth.js | 2 +- api/src/lib/owner.js | 23 +- api/src/services/helpers.js | 3 +- .../services/partReactions/partReactions.js | 18 +- api/src/services/parts/parts.js | 19 +- api/src/services/users/users.js | 27 ++- package.json | 10 +- web/src/components/Breadcrumb/Breadcrumb.js | 23 +- web/src/components/Button/Button.js | 38 +-- web/src/components/Button/Button.stories.js | 10 +- .../components/EditUser2Cell/EditUser2Cell.js | 20 +- .../components/EmojiReaction/EmojiReaction.js | 60 +++-- web/src/components/IdePartCell/IdePartCell.js | 3 +- .../components/ImageUploader/ImageUploader.js | 149 +++++++----- .../ImageUploader/ImageUploader.stories.js | 40 ++-- web/src/components/InputText/InputText.js | 20 +- web/src/components/Part2Cell/Part2Cell.js | 86 ++++--- web/src/components/PartProfile/PartProfile.js | 221 ++++++++++-------- web/src/components/Parts/Parts.js | 48 ++-- .../ProfileTextInput/ProfileTextInput.js | 34 +-- web/src/components/Svg/Svg.js | 133 ++++++++--- web/src/components/User2Cell/User2Cell.js | 2 +- web/src/components/UserProfile/UserProfile.js | 88 ++++--- web/src/helpers/emote.js | 7 +- web/src/layouts/MainLayout/MainLayout.js | 112 +++++---- web/src/pages/EditPart2Page/EditPart2Page.js | 2 +- web/src/pages/EditUser2Page/EditUser2Page.js | 2 +- web/src/pages/HomePage/HomePage.js | 111 ++++++--- web/src/pages/IdePartPage/IdePartPage.js | 2 +- web/src/pages/NewPart2Page/NewPart2Page.js | 9 +- web/src/pages/Part2Page/Part2Page.js | 2 +- 33 files changed, 852 insertions(+), 481 deletions(-) diff --git a/api/src/functions/identity-signup.js b/api/src/functions/identity-signup.js index b9f3460..ee6fa0a 100644 --- a/api/src/functions/identity-signup.js +++ b/api/src/functions/identity-signup.js @@ -68,11 +68,12 @@ export const handler = async (req, _context) => { const isUnique = !(await db.user.findOne({ where: { userName: seed }, })) - if(isUnique) { + if (isUnique) { return seed } count += 1 - const newSeed = count === 1 ? `${seed}_${count}` : seed.slice(0,-1) + count + const newSeed = + count === 1 ? `${seed}_${count}` : seed.slice(0, -1) + count return generateUniqueUserName(newSeed, count) } const userNameSeed = enforceAlphaNumeric(email.split('@')[0]) @@ -83,7 +84,7 @@ export const handler = async (req, _context) => { name: user.user_metadata && user.user_metadata.full_name, id: user.id, } - await createUserInsecure({input}) + await createUserInsecure({ input }) return { statusCode: 200, diff --git a/api/src/graphql/parts.sdl.js b/api/src/graphql/parts.sdl.js index a7f42e4..31dd970 100644 --- a/api/src/graphql/parts.sdl.js +++ b/api/src/graphql/parts.sdl.js @@ -16,7 +16,7 @@ export const schema = gql` type Query { parts: [Part!]! part(id: String!): Part - partByUserAndTitle(userName: String! partTitle: String!): Part + partByUserAndTitle(userName: String!, partTitle: String!): Part } input CreatePartInput { diff --git a/api/src/lib/auth.js b/api/src/lib/auth.js index 3889bbc..9fbd050 100644 --- a/api/src/lib/auth.js +++ b/api/src/lib/auth.js @@ -142,7 +142,7 @@ export const requireAuth = ({ role } = {}) => { throw new ForbiddenError("You don't have access to do that.") } - if(context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') { + if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') { throw new ForbiddenError("That's a local admin ONLY.") } } diff --git a/api/src/lib/owner.js b/api/src/lib/owner.js index 086deeb..dc60aff 100644 --- a/api/src/lib/owner.js +++ b/api/src/lib/owner.js @@ -7,37 +7,38 @@ export const requireOwnership = async ({ userId, userName, partId } = {}) => { if (!context.currentUser) { throw new AuthenticationError("You don't have permission to do that.") } - if(!userId && !userName && !partId) { + if (!userId && !userName && !partId) { throw new ForbiddenError("You don't have access to do that.") } - if(context.currentUser.roles?.includes('admin')) { + if (context.currentUser.roles?.includes('admin')) { return } const netlifyUserId = context.currentUser?.sub - if(userId && userId !== netlifyUserId) { + if (userId && userId !== netlifyUserId) { throw new ForbiddenError("You don't own this resource.") } - if(userName) { + if (userName) { const user = await db.user.findOne({ where: { userName }, }) - if(!user || user.id !== netlifyUserId) { + 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 (partId) { + const user = await db.part + .findOne({ + where: { id: partId }, + }) + .user() - if(!user || user.id !== netlifyUserId) { + if (!user || user.id !== netlifyUserId) { throw new ForbiddenError("You don't own this resource.") } } - } diff --git a/api/src/services/helpers.js b/api/src/services/helpers.js index d6e21f3..c5611eb 100644 --- a/api/src/services/helpers.js +++ b/api/src/services/helpers.js @@ -12,4 +12,5 @@ export const foreignKeyReplacement = (input) => { return output } -export const enforceAlphaNumeric = (string) => string.replace(/([^a-zA-Z\d_:])/g, '-') +export const enforceAlphaNumeric = (string) => + string.replace(/([^a-zA-Z\d_:])/g, '-') diff --git a/api/src/services/partReactions/partReactions.js b/api/src/services/partReactions/partReactions.js index 4efd545..9116014 100644 --- a/api/src/services/partReactions/partReactions.js +++ b/api/src/services/partReactions/partReactions.js @@ -18,20 +18,24 @@ export const partReaction = ({ id }) => { export const togglePartReaction = async ({ input }) => { // if write fails emote_userId_partId @@unique constraint, then delete it instead requireAuth() - await requireOwnership({userId: input?.userId}) + 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(', ')}`) + 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{ + 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) { + } catch (e) { dbPromise = db.partReaction.delete({ - where: { emote_userId_partId: inputClone}, + where: { emote_userId_partId: inputClone }, }) } return dbPromise diff --git a/api/src/services/parts/parts.js b/api/src/services/parts/parts.js index 260442c..aad2a72 100644 --- a/api/src/services/parts/parts.js +++ b/api/src/services/parts/parts.js @@ -1,5 +1,8 @@ import { db } from 'src/lib/db' -import { foreignKeyReplacement, enforceAlphaNumeric } from 'src/services/helpers' +import { + foreignKeyReplacement, + enforceAlphaNumeric, +} from 'src/services/helpers' import { requireAuth } from 'src/lib/auth' import { requireOwnership } from 'src/lib/owner' @@ -15,15 +18,15 @@ export const part = ({ id }) => { export const partByUserAndTitle = async ({ userName, partTitle }) => { const user = await db.user.findOne({ where: { - userName - } + userName, + }, }) return db.part.findOne({ where: { title_userId: { title: partTitle, userId: user.id, - } + }, }, }) } @@ -37,8 +40,8 @@ export const createPart = async ({ input }) => { export const updatePart = async ({ id, input }) => { requireAuth() - await requireOwnership({partId: id}) - if(input.title) { + await requireOwnership({ partId: id }) + if (input.title) { input.title = enforceAlphaNumeric(input.title) } return db.part.update({ @@ -59,5 +62,7 @@ export const Part = { 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}}), + db.part + .findOne({ where: { id: root.id } }) + .Reaction({ where: { userId: _obj.userId } }), } diff --git a/api/src/services/users/users.js b/api/src/services/users/users.js index c06e1bf..4292baa 100644 --- a/api/src/services/users/users.js +++ b/api/src/services/users/users.js @@ -23,7 +23,7 @@ export const userName = ({ userName }) => { export const createUser = ({ input }) => { requireAuth({ role: 'admin' }) - createUserInsecure({input}) + createUserInsecure({ input }) } export const createUserInsecure = ({ input }) => { return db.user.create({ @@ -41,12 +41,15 @@ export const updateUser = ({ id, input }) => { export const updateUserByUserName = async ({ userName, input }) => { requireAuth() - await requireOwnership({userName}) - if(input.userName) { + await requireOwnership({ userName }) + if (input.userName) { input.userName = enforceAlphaNumeric(input.userName) } - 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 `) + 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, @@ -63,10 +66,16 @@ export const deleteUser = ({ 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, - }}}), + 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 }) => diff --git a/package.json b/package.json index 8f4dc54..ea70fa9 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,7 @@ "@redwoodjs/core": "^0.19.2" }, "eslintConfig": { - "extends": "@redwoodjs/eslint-config", - "workingDirectories": [ - "./api", - "./web/src/components", - "./web/src/layouts", - "./web/src/pages", - "./web/src/index.js", - "./web/src/Routes.js" - ] + "extends": "@redwoodjs/eslint-config" }, "engines": { "node": ">=12", diff --git a/web/src/components/Breadcrumb/Breadcrumb.js b/web/src/components/Breadcrumb/Breadcrumb.js index fe921fb..1057db4 100644 --- a/web/src/components/Breadcrumb/Breadcrumb.js +++ b/web/src/components/Breadcrumb/Breadcrumb.js @@ -1,20 +1,31 @@ -import { getActiveClasses } from "get-active-classes" +import { getActiveClasses } from 'get-active-classes' import InputText from 'src/components/InputText' const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => { return ( -

      -
      .
      - +

      +
      + . +
      + {userName} -
      .
      +
      + . +

      ) diff --git a/web/src/components/Button/Button.js b/web/src/components/Button/Button.js index 5d76f0f..28b1056 100644 --- a/web/src/components/Button/Button.js +++ b/web/src/components/Button/Button.js @@ -1,21 +1,31 @@ import { getActiveClasses } from 'get-active-classes' import Svg from 'src/components/Svg' -const Button = ({onClick, iconName, children, className, shouldAnimateHover, disabled}) => { +const Button = ({ + onClick, + iconName, + children, + className, + shouldAnimateHover, + disabled, +}) => { return ( - + ) } diff --git a/web/src/components/Button/Button.stories.js b/web/src/components/Button/Button.stories.js index 7d95d2d..115369e 100644 --- a/web/src/components/Button/Button.stories.js +++ b/web/src/components/Button/Button.stories.js @@ -1,10 +1,12 @@ import Button from './Button' export const generated = () => { - return <> - button with icon - - + return ( + <> + button with icon + + + ) } export default { title: 'Components/Button' } diff --git a/web/src/components/EditUser2Cell/EditUser2Cell.js b/web/src/components/EditUser2Cell/EditUser2Cell.js index 850f260..94a7d24 100644 --- a/web/src/components/EditUser2Cell/EditUser2Cell.js +++ b/web/src/components/EditUser2Cell/EditUser2Cell.js @@ -35,8 +35,8 @@ export const Failure = ({ error }) =>
      Error: {error.message}
      export const Success = ({ user }) => { const { addMessage } = useFlash() const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, { - onCompleted: ({updateUserByUserName}) => { - navigate(routes.user2({userName: updateUserByUserName.userName})) + onCompleted: ({ updateUserByUserName }) => { + navigate(routes.user2({ userName: updateUserByUserName.userName })) addMessage('User updated.', { classes: 'rw-flash-success' }) }, }) @@ -45,11 +45,13 @@ export const Success = ({ user }) => { updateUser({ variables: { userName, input } }) } - return + return ( + + ) } diff --git a/web/src/components/EmojiReaction/EmojiReaction.js b/web/src/components/EmojiReaction/EmojiReaction.js index f4dd910..ce254b9 100644 --- a/web/src/components/EmojiReaction/EmojiReaction.js +++ b/web/src/components/EmojiReaction/EmojiReaction.js @@ -1,5 +1,5 @@ import { useState } from 'react' -import { getActiveClasses } from "get-active-classes" +import { getActiveClasses } from 'get-active-classes' import Popover from '@material-ui/core/Popover' import { useAuth } from '@redwoodjs/auth' @@ -7,14 +7,21 @@ import Svg from 'src/components/Svg' const emojiMenu = ['❤️', '👍', '😄', '🙌'] // const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌'] -const noEmotes =[{ - emoji: '❤️', - count: 0, -}] +const noEmotes = [ + { + emoji: '❤️', + count: 0, + }, +] -const textShadow = {textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'} +const textShadow = { textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)' } -const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => { +const EmojiReaction = ({ + emotes, + userEmotes, + onEmote = () => {}, + className, +}) => { const { currentUser } = useAuth() const [isOpen, setIsOpen] = useState(false) const [anchorEl, setAnchorEl] = useState(null) @@ -48,29 +55,40 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => return ( <> -
      -
      -
      - -
      +
      +
      +
      +
      +
      {(emotes.length ? emotes : noEmotes).map((emote, i) => ( handleEmojiClick(emote.emoji)} > - {emote.emoji}{emote.count} + {emote.emoji} + {emote.count} ))}
      @@ -96,7 +114,9 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => style={textShadow} key={`${emoji}-${i}}`} onClick={() => handleEmojiClick(emoji)} - >{emoji} + > + {emoji} + ))}
      diff --git a/web/src/components/IdePartCell/IdePartCell.js b/web/src/components/IdePartCell/IdePartCell.js index 122ed88..703f70b 100644 --- a/web/src/components/IdePartCell/IdePartCell.js +++ b/web/src/components/IdePartCell/IdePartCell.js @@ -35,8 +35,7 @@ export const Success = ({ part }) => { addMessage('Part updated.', { classes: 'rw-flash-success' }) }, }) - console.log({updatePart}) - + console.log({ updatePart }) const saveCode = (input, id) => { console.log(id, input, 'wowow') diff --git a/web/src/components/ImageUploader/ImageUploader.js b/web/src/components/ImageUploader/ImageUploader.js index ca93f5e..3534293 100644 --- a/web/src/components/ImageUploader/ImageUploader.js +++ b/web/src/components/ImageUploader/ImageUploader.js @@ -1,6 +1,6 @@ -import React, { useCallback, useState } from "react"; -import { useDropzone } from "react-dropzone"; -import Button from "@material-ui/core/Button"; +import React, { useCallback, useState } from 'react' +import { useDropzone } from 'react-dropzone' +import Button from '@material-ui/core/Button' import axios from 'axios' import ReactCrop from 'react-image-crop' import { Dialog } from '@material-ui/core' @@ -8,17 +8,17 @@ import { Image as CloudinaryImage } from 'cloudinary-react' import 'react-image-crop/dist/ReactCrop.css' 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"; +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, - aspectRatio, - className, - isEditable, - width=600 - }) { + onImageUpload, + imageUrl, + aspectRatio, + className, + isEditable, + width = 600, +}) { const [isModalOpen, setIsModalOpen] = useState(false) const [file, setFile] = useState() const [cloudinaryId, setCloudinaryId] = useState(imageUrl) @@ -27,17 +27,17 @@ export default function ImageUploader({ aspect: aspectRatio, unit: '%', width: 100, - }); + }) async function handleImageUpload() { const croppedFile = await getCroppedImg(imageObj, crop, 'avatar') - const imageData = new FormData(); - imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET); - imageData.append('file', croppedFile); + const imageData = new FormData() + imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET) + imageData.append('file', croppedFile) let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData) try { const { data } = await upload - if (data && data.public_id !== "") { - onImageUpload({cloudinaryPublicId: data.public_id}) + if (data && data.public_id !== '') { + onImageUpload({ cloudinaryPublicId: data.public_id }) setCloudinaryId(data.public_id) setIsModalOpen(false) } @@ -46,62 +46,85 @@ export default function ImageUploader({ } } // Drag and Drop - const onDrop = useCallback(acceptedFiles => { + const onDrop = useCallback((acceptedFiles) => { setIsModalOpen(true) const fileReader = new FileReader() fileReader.onload = () => { setFile(fileReader.result) } fileReader.readAsDataURL(acceptedFiles[0]) - }, []); + }, []) - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }) return ( -
      +
      - {cloudinaryId && isEditable && } + {cloudinaryId && isEditable && ( + + )} {isEditable && } - {(cloudinaryId || !isEditable) &&
      - -
      } - {!cloudinaryId && } - {!cloudinaryId && isEditable &&
      -
      - Drop files here ... - or - upload - + {(cloudinaryId || !isEditable) && ( +
      +
      -
      } + )} + {!cloudinaryId && } + {!cloudinaryId && isEditable && ( +
      +
      + Drop files here ... or{' '} + + + upload + + +
      +
      + )}
      - setIsModalOpen(false)} - > + setIsModalOpen(false)}>
      - setImageObj(image)} onChange={newCrop => setCrop(newCrop)} /> - + setImageObj(image)} + onChange={(newCrop) => setCrop(newCrop)} + /> +
      - ); + ) } function getCroppedImg(image, crop, fileName) { - const canvas = document.createElement('canvas'); - const scaleX = image.naturalWidth / image.width; - const scaleY = image.naturalHeight / image.height; - canvas.width = crop.width; - canvas.height = crop.height; - const ctx = canvas.getContext('2d'); + const canvas = document.createElement('canvas') + const scaleX = image.naturalWidth / image.width + const scaleY = image.naturalHeight / image.height + canvas.width = crop.width + canvas.height = crop.height + const ctx = canvas.getContext('2d') ctx.drawImage( image, crop.x * scaleX, @@ -112,16 +135,20 @@ function getCroppedImg(image, crop, fileName) { 0, crop.width, crop.height - ); + ) // As Base64 string // const base64Image = canvas.toDataURL('image/jpeg'); // As a blob return new Promise((resolve, reject) => { - canvas.toBlob(blob => { - blob.name = fileName; - resolve(blob); - }, 'image/jpeg', 1); - }); + canvas.toBlob( + (blob) => { + blob.name = fileName + resolve(blob) + }, + 'image/jpeg', + 1 + ) + }) } diff --git a/web/src/components/ImageUploader/ImageUploader.stories.js b/web/src/components/ImageUploader/ImageUploader.stories.js index f5b9f65..e2a49bc 100644 --- a/web/src/components/ImageUploader/ImageUploader.stories.js +++ b/web/src/components/ImageUploader/ImageUploader.stories.js @@ -4,35 +4,39 @@ export const generated = () => { return ( <>

      AspectRatio:1, no initial image, editable

      - < - ImageUploader - onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)} + + console.log(cloudinaryPublicId) + } aspectRatio={1} isEditable={true} - className={"bg-red-400 rounded-half rounded-br-xl"} + className={'bg-red-400 rounded-half rounded-br-xl'} />

      AspectRatio 16:9, no initial image, editable

      - < - ImageUploader - onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)} - aspectRatio={16/9} + + console.log(cloudinaryPublicId) + } + aspectRatio={16 / 9} isEditable={true} - className={"bg-red-400 rounded-xl"} + className={'bg-red-400 rounded-xl'} imageUrl="CadHub/inakek2urbreynblzhgt" />

      AspectRatio:1, no initial image, NOT editable

      - < - ImageUploader - onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)} + + console.log(cloudinaryPublicId) + } aspectRatio={1} - className={"rounded-half rounded-br-xl"} + className={'rounded-half rounded-br-xl'} />

      AspectRatio ,16:9 no initial image, NOT editable

      - < - ImageUploader - onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)} - aspectRatio={16/9} - className={"rounded-xl"} + + console.log(cloudinaryPublicId) + } + aspectRatio={16 / 9} + className={'rounded-xl'} imageUrl="CadHub/inakek2urbreynblzhgt" /> diff --git a/web/src/components/InputText/InputText.js b/web/src/components/InputText/InputText.js index 93826dd..aad823b 100644 --- a/web/src/components/InputText/InputText.js +++ b/web/src/components/InputText/InputText.js @@ -1,9 +1,15 @@ import { getActiveClasses } from 'get-active-classes' -const InputText = ({value, isEditable, onChange ,className}) => { +const InputText = ({ value, isEditable, onChange, className }) => { return ( <> -
      +
      { type="text" />
      - {value} + + {value} + ) } diff --git a/web/src/components/Part2Cell/Part2Cell.js b/web/src/components/Part2Cell/Part2Cell.js index a9016c4..ecfc449 100644 --- a/web/src/components/Part2Cell/Part2Cell.js +++ b/web/src/components/Part2Cell/Part2Cell.js @@ -5,7 +5,11 @@ 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) { + query FIND_PART_BY_USERNAME_TITLE( + $userName: String! + $partTitle: String + $currentUserId: String + ) { userPart: userName(userName: $userName) { id name @@ -42,7 +46,7 @@ export const QUERY = gql` const UPDATE_PART_MUTATION = gql` mutation UpdatePartMutation($id: String!, $input: UpdatePartInput!) { - updatePart:updatePart(id: $id, input: $input) { + updatePart: updatePart(id: $id, input: $input) { id title user { @@ -66,7 +70,7 @@ const CREATE_PART_MUTATION = gql` ` const TOGGLE_REACTION_MUTATION = gql` mutation ToggleReactionMutation($input: TogglePartReactionInput!) { - togglePartReaction(input: $input){ + togglePartReaction(input: $input) { id emote } @@ -87,23 +91,33 @@ export const Empty = () =>
      Empty
      export const Failure = ({ error }) =>
      Error: {error.message}
      -export const Success = ({ userPart, variables: {isEditable}, refetch}) => { +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})) + 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})) + 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) { + if (!id) { createUser({ variables: { input } }) return } @@ -111,30 +125,42 @@ export const Success = ({ userPart, variables: {isEditable}, refetch}) => { } const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, { - onCompleted: () => refetch() + onCompleted: () => refetch(), }) - const onReaction = (emote) => toggleReaction({variables: {input: { - emote, - userId: currentUser.sub, - partId: userPart?.Part?.id, - }}}) + const onReaction = (emote) => + toggleReaction({ + variables: { + input: { + emote, + userId: currentUser.sub, + partId: userPart?.Part?.id, + }, + }, + }) const [createComment] = useMutation(CREATE_COMMENT_MUTATION, { - onCompleted: () => refetch() + onCompleted: () => refetch(), }) - const onComment = (text) => createComment({variables: {input: { - text, - userId: currentUser.sub, - partId: userPart?.Part?.id, - }}}) + const onComment = (text) => + createComment({ + variables: { + input: { + text, + userId: currentUser.sub, + partId: userPart?.Part?.id, + }, + }, + }) - return + return ( + + ) } diff --git a/web/src/components/PartProfile/PartProfile.js b/web/src/components/PartProfile/PartProfile.js index 9a7facc..00beef0 100644 --- a/web/src/components/PartProfile/PartProfile.js +++ b/web/src/components/PartProfile/PartProfile.js @@ -1,58 +1,69 @@ -import {useState, useEffect} from 'react' +import { useState, useEffect } from 'react' import { useAuth } from '@redwoodjs/auth' import { Link, navigate, routes } from '@redwoodjs/router' -import Editor from "rich-markdown-editor"; +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'; +import { getActiveClasses } from 'get-active-classes' const PartProfile = ({ - userPart, - isEditable, - onSave, - loading, - error, - onReaction, - onComment, - }) => { + 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 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 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})) + navigate( + routes.editPart2({ userName: userPart.userName, partTitle: part.title }) + ) } return ( <> -
      - +
      {/* Side column */} {/* main project center column */}
      - - { !!(input?.mainImage || isEditable) && } -
      + + {!!(input?.mainImage || isEditable) && ( + + )} +
      - - {/* comments */} - { !isEditable && <> -
      -

      Comments

      + {!isEditable && ( + <> +
      +

      + Comments +

      -
        - {part?.Comment.map(({text, user, id}) => ( -
      • -
        - {}} - aspectRatio={1} - imageUrl={user?.image} - width={50} +
          + {part?.Comment.map(({ text, user, id }) => ( +
        • +
          + {}} + aspectRatio={1} + imageUrl={user?.image} + width={50} + /> +
          +
          +
          + + {user.userName} + +
          +
          + {text} +
          +
          +
        • + ))} +
        + {currentUser && ( + <> +
        +