Merge pull request #533 from Irev-Dev/frank/add-project-forking
Add project forking
This commit was merged in pull request #533.
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Project" ADD COLUMN "forkedFromId" TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Project" ADD FOREIGN KEY ("forkedFromId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -53,9 +53,12 @@ model Project {
|
|||||||
deleted Boolean @default(false)
|
deleted Boolean @default(false)
|
||||||
cadPackage CadPackage @default(openscad)
|
cadPackage CadPackage @default(openscad)
|
||||||
socialCard SocialCard?
|
socialCard SocialCard?
|
||||||
|
forkedFromId String?
|
||||||
|
forkedFrom Project? @relation("Fork", fields: [forkedFromId], references: [id])
|
||||||
|
|
||||||
Comment Comment[]
|
childForks Project[] @relation("Fork")
|
||||||
Reaction ProjectReaction[]
|
Comment Comment[]
|
||||||
|
Reaction ProjectReaction[]
|
||||||
@@unique([title, userId])
|
@@unique([title, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export const schema = gql`
|
|||||||
socialCard: SocialCard
|
socialCard: SocialCard
|
||||||
Comment: [Comment]!
|
Comment: [Comment]!
|
||||||
Reaction(userId: String): [ProjectReaction]!
|
Reaction(userId: String): [ProjectReaction]!
|
||||||
|
forkedFromId: String
|
||||||
|
forkedFrom: Project
|
||||||
|
childForks: [Project]!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CadPackage {
|
enum CadPackage {
|
||||||
@@ -37,6 +40,11 @@ export const schema = gql`
|
|||||||
cadPackage: CadPackage!
|
cadPackage: CadPackage!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ForkProjectInput {
|
||||||
|
userId: String!
|
||||||
|
forkedFromId: String
|
||||||
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
title: String
|
title: String
|
||||||
description: String
|
description: String
|
||||||
@@ -47,7 +55,7 @@ export const schema = gql`
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createProject(input: CreateProjectInput!): Project!
|
createProject(input: CreateProjectInput!): Project!
|
||||||
forkProject(input: CreateProjectInput!): Project!
|
forkProject(input: ForkProjectInput!): Project!
|
||||||
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
||||||
updateProjectImages(
|
updateProjectImages(
|
||||||
id: String!
|
id: String!
|
||||||
|
|||||||
@@ -78,13 +78,27 @@ export const createProject = async ({ input }: CreateProjectArgs) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const forkProject = async ({ input }) => {
|
export const forkProject = async ({ input }) => {
|
||||||
// Only difference between create and fork project is that fork project will generate a unique title
|
requireAuth()
|
||||||
// (for the user) if there is a conflict
|
const projectData = await db.project.findUnique({
|
||||||
|
where: {
|
||||||
|
id: input.forkedFromId,
|
||||||
|
},
|
||||||
|
})
|
||||||
const isUniqueCallback = isUniqueProjectTitle(input.userId)
|
const isUniqueCallback = isUniqueProjectTitle(input.userId)
|
||||||
const title = await generateUniqueString(input.title, isUniqueCallback)
|
let title = projectData.title
|
||||||
// TODO change the description to `forked from userName/projectName ${rest of description}`
|
|
||||||
|
title = await generateUniqueString(title, isUniqueCallback)
|
||||||
|
|
||||||
|
const { code, description, cadPackage } = projectData
|
||||||
|
|
||||||
return db.project.create({
|
return db.project.create({
|
||||||
data: foreignKeyReplacement({ ...input, title }),
|
data: foreignKeyReplacement({
|
||||||
|
...input,
|
||||||
|
title,
|
||||||
|
code,
|
||||||
|
description,
|
||||||
|
cadPackage,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +266,11 @@ export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Project = {
|
export const Project = {
|
||||||
|
forkedFrom: (_obj, { root }) =>
|
||||||
|
root.forkedFromId &&
|
||||||
|
db.project.findUnique({ where: { id: root.forkedFromId } }),
|
||||||
|
childForks: (_obj, { root }) =>
|
||||||
|
db.project.findMany({ where: { forkedFromId: root.id } }),
|
||||||
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||||
db.project.findUnique({ where: { id: root.id } }).user(),
|
db.project.findUnique({ where: { id: root.id } }).user(),
|
||||||
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { useMutation } from '@redwoodjs/web'
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
|
||||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
|
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
|
||||||
|
|
||||||
import FullScriptEncoding from 'src/components/EncodedUrl/FullScriptEncoding'
|
import FullScriptEncoding from 'src/components/EncodedUrl/FullScriptEncoding'
|
||||||
import ExternalScript from 'src/components/EncodedUrl/ExternalScript'
|
import ExternalScript from 'src/components/EncodedUrl/ExternalScript'
|
||||||
import Svg from 'src/components/Svg/Svg'
|
import Svg from 'src/components/Svg/Svg'
|
||||||
import NavPlusButton from 'src/components/NavPlusButton'
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
import ProfileSlashLogin from 'src/components/ProfileSlashLogin'
|
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
|
||||||
|
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||||
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
|
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
|
||||||
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
|
|
||||||
import { ReactNode } from 'react'
|
const FORK_PROJECT_MUTATION = gql`
|
||||||
|
mutation ForkProjectMutation($input: ForkProjectInput!) {
|
||||||
|
forkProject(input: $input) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
userName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const TopButton = ({
|
const TopButton = ({
|
||||||
onClick,
|
onClick,
|
||||||
@@ -38,180 +51,170 @@ const TopButton = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IdeHeaderProps {
|
export default function IdeHeader({
|
||||||
handleRender: () => void
|
|
||||||
projectTitle?: string
|
|
||||||
projectOwner?: string
|
|
||||||
projectOwnerId?: string
|
|
||||||
projectOwnerImage?: string
|
|
||||||
projectId?: string
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const IdeHeader = ({
|
|
||||||
handleRender,
|
handleRender,
|
||||||
projectOwner,
|
context,
|
||||||
projectTitle,
|
}: {
|
||||||
projectOwnerImage,
|
handleRender?: () => void
|
||||||
projectId,
|
context: 'ide' | 'profile'
|
||||||
projectOwnerId,
|
}) {
|
||||||
children,
|
|
||||||
}: IdeHeaderProps) => {
|
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const { project } = useIdeContext()
|
const { project } = useIdeContext()
|
||||||
|
|
||||||
|
const isProfile = context === 'profile'
|
||||||
const canEdit =
|
const canEdit =
|
||||||
(currentUser &&
|
(currentUser && currentUser?.sub === project?.user?.id) ||
|
||||||
currentUser?.sub === (project?.user?.id || projectOwnerId)) ||
|
currentUser?.roles?.includes('admin')
|
||||||
currentUser?.roles.includes('admin')
|
const projectOwner = project?.user?.userName
|
||||||
const _projectId = projectId || project?.id
|
|
||||||
const _projectOwner = project?.user?.userName || projectOwner
|
|
||||||
|
|
||||||
return (
|
const [createFork] = useMutation(FORK_PROJECT_MUTATION, {
|
||||||
<div className="h-16 w-full bg-ch-gray-900 flex justify-between items-center text-lg">
|
onCompleted: ({ forkProject }) => {
|
||||||
<div className="h-full text-gray-300 flex items-center">
|
const params = {
|
||||||
<div className="w-14 h-16 flex items-center justify-center bg-ch-gray-900">
|
userName: forkProject?.user?.userName,
|
||||||
<Link to={routes.home()}>
|
projectTitle: forkProject?.title,
|
||||||
<Svg className="w-12 p-0.5" name="favicon" />
|
}
|
||||||
</Link>
|
navigate(!isProfile ? routes.ide(params) : routes.project(params))
|
||||||
</div>
|
},
|
||||||
{_projectId && (
|
})
|
||||||
<>
|
const handleFork = () => {
|
||||||
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
|
const prom = createFork({
|
||||||
<Gravatar
|
variables: {
|
||||||
image={project?.user?.image || projectOwnerImage}
|
input: {
|
||||||
className="w-10"
|
userId: currentUser.sub,
|
||||||
/>
|
forkedFromId: project?.id,
|
||||||
<Link
|
},
|
||||||
to={routes.user({
|
},
|
||||||
userName: _projectOwner,
|
})
|
||||||
})}
|
toast.promise(prom, {
|
||||||
>
|
loading: 'Forking...',
|
||||||
{_projectOwner}
|
success: <b>Forked successfully!</b>,
|
||||||
</Link>
|
error: <b>Problem forking.</b>,
|
||||||
</span>
|
})
|
||||||
<EditableProjectTitle
|
}
|
||||||
id={_projectId}
|
|
||||||
userName={_projectOwner}
|
|
||||||
projectTitle={project?.title || projectTitle}
|
|
||||||
canEdit={canEdit}
|
|
||||||
shouldRouteToIde={!projectTitle}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
|
|
||||||
{!children ? (
|
|
||||||
<DefaultTopButtons
|
|
||||||
project={project}
|
|
||||||
projectTitle={projectTitle}
|
|
||||||
_projectOwner={_projectOwner}
|
|
||||||
handleRender={handleRender}
|
|
||||||
canEdit={canEdit}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
{/* <TopButton>Fork</TopButton> */}
|
|
||||||
<div className="h-8 w-8">
|
|
||||||
<NavPlusButton />
|
|
||||||
</div>
|
|
||||||
<ProfileSlashLogin />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IdeHeader
|
|
||||||
|
|
||||||
function DefaultTopButtons({
|
|
||||||
project,
|
|
||||||
projectTitle,
|
|
||||||
_projectOwner,
|
|
||||||
handleRender,
|
|
||||||
canEdit,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{canEdit && !projectTitle && (
|
<div className="flex justify-between flex-grow h-full">
|
||||||
<CaptureButton
|
<div className="flex h-full items-center text-gray-300">
|
||||||
canEdit={canEdit}
|
{project?.id && (
|
||||||
projectTitle={project?.title}
|
<>
|
||||||
userName={project?.user?.userName}
|
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
|
||||||
shouldUpdateImage={!project?.mainImage}
|
<Gravatar image={project?.user?.image} className="w-10" />
|
||||||
TheButton={({ onClick }) => (
|
<Link
|
||||||
|
to={routes.user({
|
||||||
|
userName: projectOwner,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{projectOwner}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<EditableProjectTitle
|
||||||
|
id={project?.id}
|
||||||
|
userName={projectOwner}
|
||||||
|
projectTitle={project?.title}
|
||||||
|
canEdit={canEdit}
|
||||||
|
shouldRouteToIde={!isProfile}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-flow-col-dense gap-4 items-center mr-4">
|
||||||
|
{canEdit && !isProfile && (
|
||||||
|
<CaptureButton
|
||||||
|
canEdit={canEdit}
|
||||||
|
projectTitle={project?.title}
|
||||||
|
userName={project?.user?.userName}
|
||||||
|
shouldUpdateImage={!project?.mainImage}
|
||||||
|
TheButton={({ onClick }) => (
|
||||||
|
<TopButton
|
||||||
|
onClick={onClick}
|
||||||
|
name="Save Project Image"
|
||||||
|
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||||
|
>
|
||||||
|
<Svg name="camera" className="w-6 h-6 text-ch-blue-400" />
|
||||||
|
</TopButton>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isProfile && (
|
||||||
<TopButton
|
<TopButton
|
||||||
onClick={onClick}
|
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||||
name="Save Project Image"
|
onClick={handleRender}
|
||||||
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
name={canEdit ? 'Save' : 'Preview'}
|
||||||
>
|
>
|
||||||
<Svg name="camera" className="w-6 h-6 text-ch-blue-400" />
|
<Svg
|
||||||
|
name={canEdit ? 'floppy-disk' : 'photograph'}
|
||||||
|
className="w-6 h-6 text-ch-pink-500"
|
||||||
|
/>
|
||||||
</TopButton>
|
</TopButton>
|
||||||
)}
|
)}
|
||||||
/>
|
{isProfile && (
|
||||||
)}
|
<TopButton
|
||||||
{!projectTitle && (
|
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||||
<TopButton
|
onClick={() =>
|
||||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
navigate(
|
||||||
onClick={handleRender}
|
routes.ide({
|
||||||
name={canEdit ? 'Save' : 'Preview'}
|
userName: projectOwner,
|
||||||
>
|
projectTitle: project.title,
|
||||||
<Svg
|
})
|
||||||
name={canEdit ? 'floppy-disk' : 'photograph'}
|
)
|
||||||
className="w-6 h-6 text-ch-pink-500"
|
}
|
||||||
/>
|
name="Editor"
|
||||||
</TopButton>
|
>
|
||||||
)}
|
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
|
||||||
{projectTitle && (
|
</TopButton>
|
||||||
<TopButton
|
)}
|
||||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
<Popover className="relative outline-none w-full h-full">
|
||||||
onClick={() =>
|
{({ open }) => {
|
||||||
navigate(routes.ide({ userName: _projectOwner, projectTitle }))
|
return (
|
||||||
}
|
<>
|
||||||
name="Editor"
|
<Popover.Button className="h-full outline-none">
|
||||||
>
|
<TopButton
|
||||||
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
|
Tag="div"
|
||||||
</TopButton>
|
name="Share"
|
||||||
)}
|
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||||
<Popover className="relative outline-none w-full h-full">
|
>
|
||||||
{({ open }) => {
|
<Svg
|
||||||
return (
|
name="share"
|
||||||
<>
|
className="w-6 h-6 text-ch-purple-500 mt-1"
|
||||||
<Popover.Button className="h-full w-full outline-none">
|
/>
|
||||||
<TopButton
|
</TopButton>
|
||||||
Tag="div"
|
</Popover.Button>
|
||||||
name="Share"
|
{open && (
|
||||||
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
<Popover.Panel className="absolute z-10 mt-4 right-0">
|
||||||
>
|
<Tabs
|
||||||
<Svg
|
className="bg-ch-purple-gray-200 rounded-md shadow-md overflow-hidden text-gray-700"
|
||||||
name="share"
|
selectedTabClassName="bg-ch-gray-700 text-white"
|
||||||
className="w-6 h-6 text-ch-purple-500 mt-1"
|
>
|
||||||
/>
|
<TabPanel>
|
||||||
</TopButton>
|
<FullScriptEncoding />
|
||||||
</Popover.Button>
|
</TabPanel>
|
||||||
{open && (
|
<TabPanel>
|
||||||
<Popover.Panel className="absolute z-10 mt-4 right-0">
|
<ExternalScript />
|
||||||
<Tabs
|
</TabPanel>
|
||||||
className="bg-ch-purple-gray-200 rounded-md shadow-md overflow-hidden text-gray-700"
|
|
||||||
selectedTabClassName="bg-ch-gray-700 text-white"
|
|
||||||
>
|
|
||||||
<TabPanel>
|
|
||||||
<FullScriptEncoding />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<ExternalScript />
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
|
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
|
||||||
<Tab className="p-3 px-5">encoded script</Tab>
|
<Tab className="p-3 px-5">encoded script</Tab>
|
||||||
<Tab className="p-3 px-5">external script</Tab>
|
<Tab className="p-3 px-5">external script</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
{currentUser?.sub && (
|
||||||
|
<TopButton
|
||||||
|
onClick={handleFork}
|
||||||
|
name="Fork"
|
||||||
|
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||||
|
>
|
||||||
|
<Svg name="fork-new" className="w-6 h-6 text-ch-blue-400" />
|
||||||
|
</TopButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import IdeContainer from 'src/components/IdeContainer/IdeContainer'
|
import IdeContainer from 'src/components/IdeContainer/IdeContainer'
|
||||||
import { useRender } from './useRender'
|
import { useRender } from './useRender'
|
||||||
import OutBound from 'src/components/OutBound/OutBound'
|
|
||||||
import IdeSideBar from 'src/components/IdeSideBar/IdeSideBar'
|
import IdeSideBar from 'src/components/IdeSideBar/IdeSideBar'
|
||||||
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
import TopNav from 'src/components/TopNav/TopNav'
|
||||||
import Svg from 'src/components/Svg/Svg'
|
|
||||||
import { useIdeInit } from 'src/components/EncodedUrl/helpers'
|
import { useIdeInit } from 'src/components/EncodedUrl/helpers'
|
||||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||||
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
|
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
|
||||||
import { ShortcutsModalContext } from 'src/components/EditorMenu/AllShortcutsModal'
|
import { ShortcutsModalContext } from 'src/components/EditorMenu/AllShortcutsModal'
|
||||||
|
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
||||||
|
import type { CadPackageType } from 'src/components/CadPackage/CadPackage'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cadPackage: string
|
cadPackage: CadPackageType
|
||||||
}
|
}
|
||||||
|
|
||||||
const IdeWrapper = ({ cadPackage }: Props) => {
|
const IdeWrapper = ({ cadPackage }: Props) => {
|
||||||
@@ -33,7 +33,9 @@ const IdeWrapper = ({ cadPackage }: Props) => {
|
|||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<ShortcutsModalContext.Provider value={shortcutModalContextValues}>
|
<ShortcutsModalContext.Provider value={shortcutModalContextValues}>
|
||||||
<nav className="flex">
|
<nav className="flex">
|
||||||
<IdeHeader handleRender={onRender} />
|
<TopNav>
|
||||||
|
<IdeHeader handleRender={onRender} context="ide" />
|
||||||
|
</TopNav>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="h-full flex flex-grow bg-ch-gray-900">
|
<div className="h-full flex flex-grow bg-ch-gray-900">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import { countEmotes } from 'src/helpers/emote'
|
|||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
import { ImageFallback } from '../ImageUploader/ImageUploader'
|
import { ImageFallback } from '../ImageUploader/ImageUploader'
|
||||||
|
|
||||||
const ProjectCard = ({ title, mainImage, user, Reaction, cadPackage }) => (
|
const ProjectCard = ({
|
||||||
|
title,
|
||||||
|
mainImage,
|
||||||
|
user,
|
||||||
|
Reaction,
|
||||||
|
cadPackage,
|
||||||
|
childForks,
|
||||||
|
}) => (
|
||||||
<li
|
<li
|
||||||
className="rounded p-1.5 bg-ch-gray-760 hover:bg-ch-gray-710 shadow-ch"
|
className="rounded p-1.5 bg-ch-gray-760 hover:bg-ch-gray-710 shadow-ch"
|
||||||
key={`${user?.userName}--${title}`}
|
key={`${user?.userName}--${title}`}
|
||||||
@@ -45,7 +52,8 @@ const ProjectCard = ({ title, mainImage, user, Reaction, cadPackage }) => (
|
|||||||
{countEmotes(Reaction).reduce((prev, { count }) => prev + count, 0)}
|
{countEmotes(Reaction).reduce((prev, { count }) => prev + count, 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 flex items-center bg-ch-blue-650 bg-opacity-30 text-ch-gray-300 rounded-sm">
|
<div className="px-2 flex items-center bg-ch-blue-650 bg-opacity-30 text-ch-gray-300 rounded-sm">
|
||||||
<Svg name="fork-new" className="w-4 mr-2" />0
|
<Svg name="fork-new" className="w-4 mr-2" />
|
||||||
|
{childForks?.length || 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useMutation } from '@redwoodjs/web'
|
|||||||
import { toast } from '@redwoodjs/web/toast'
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
import { navigate, routes } from '@redwoodjs/router'
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||||
|
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||||
|
|
||||||
import ProjectProfile from 'src/components/ProjectProfile/ProjectProfile'
|
import ProjectProfile from 'src/components/ProjectProfile/ProjectProfile'
|
||||||
import { QUERY as PROJECT_REACTION_QUERY } from 'src/components/ProjectReactionsCell'
|
import { QUERY as PROJECT_REACTION_QUERY } from 'src/components/ProjectReactionsCell'
|
||||||
@@ -26,8 +28,14 @@ export const QUERY = gql`
|
|||||||
mainImage
|
mainImage
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
userId
|
|
||||||
cadPackage
|
cadPackage
|
||||||
|
forkedFrom {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
user {
|
||||||
|
userName
|
||||||
|
}
|
||||||
|
}
|
||||||
Reaction {
|
Reaction {
|
||||||
emote
|
emote
|
||||||
}
|
}
|
||||||
@@ -114,26 +122,20 @@ export const Empty = () => <div className="h-full">Empty</div>
|
|||||||
|
|
||||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||||
|
|
||||||
export const Success = ({
|
export const Success = ({ userProject, refetch }) => {
|
||||||
userProject,
|
|
||||||
variables: { isEditable },
|
|
||||||
refetch,
|
|
||||||
}) => {
|
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const [updateProject, { loading, error }] = useMutation(
|
const [state, thunkDispatch] = useIdeState()
|
||||||
UPDATE_PROJECT_MUTATION,
|
const [updateProject] = useMutation(UPDATE_PROJECT_MUTATION, {
|
||||||
{
|
onCompleted: ({ updateProject }) => {
|
||||||
onCompleted: ({ updateProject }) => {
|
navigate(
|
||||||
navigate(
|
routes.project({
|
||||||
routes.project({
|
userName: updateProject.user.userName,
|
||||||
userName: updateProject.user.userName,
|
projectTitle: updateProject.title,
|
||||||
projectTitle: updateProject.title,
|
})
|
||||||
})
|
)
|
||||||
)
|
toast.success('Project updated.')
|
||||||
toast.success('Project updated.')
|
},
|
||||||
},
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
const [createProject] = useMutation(CREATE_PROJECT_MUTATION, {
|
const [createProject] = useMutation(CREATE_PROJECT_MUTATION, {
|
||||||
onCompleted: ({ createProject }) => {
|
onCompleted: ({ createProject }) => {
|
||||||
navigate(
|
navigate(
|
||||||
@@ -154,7 +156,7 @@ export const Success = ({
|
|||||||
refetch()
|
refetch()
|
||||||
}
|
}
|
||||||
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
|
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
|
||||||
onCompleted: ({ deleteProject }) => {
|
onCompleted: () => {
|
||||||
navigate(routes.home())
|
navigate(routes.home())
|
||||||
toast.success('Project deleted.')
|
toast.success('Project deleted.')
|
||||||
},
|
},
|
||||||
@@ -200,15 +202,27 @@ export const Success = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectProfile
|
<IdeContext.Provider
|
||||||
userProject={userProject}
|
value={{
|
||||||
onSave={onSave}
|
state,
|
||||||
onDelete={onDelete}
|
thunkDispatch,
|
||||||
loading={loading}
|
project: {
|
||||||
error={error}
|
...userProject?.Project,
|
||||||
isEditable={isEditable}
|
user: {
|
||||||
onReaction={onReaction}
|
id: userProject.id,
|
||||||
onComment={onComment}
|
image: userProject.image,
|
||||||
/>
|
userName: userProject.userName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProjectProfile
|
||||||
|
userProject={userProject}
|
||||||
|
onSave={onSave}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onReaction={onReaction}
|
||||||
|
onComment={onComment}
|
||||||
|
/>
|
||||||
|
</IdeContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from 'src/components/Button/Button'
|
|||||||
import ProjectReactionsCell from '../ProjectReactionsCell'
|
import ProjectReactionsCell from '../ProjectReactionsCell'
|
||||||
import { countEmotes } from 'src/helpers/emote'
|
import { countEmotes } from 'src/helpers/emote'
|
||||||
import { getActiveClasses } from 'get-active-classes'
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
|
import TopNav from 'src/components/TopNav/TopNav'
|
||||||
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
||||||
import CadPackage from 'src/components/CadPackage/CadPackage'
|
import CadPackage from 'src/components/CadPackage/CadPackage'
|
||||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||||
@@ -38,6 +39,7 @@ const ProjectProfile = ({
|
|||||||
const hasPermissionToEdit =
|
const hasPermissionToEdit =
|
||||||
currentUser?.sub === userProject.id || currentUser?.roles.includes('admin')
|
currentUser?.sub === userProject.id || currentUser?.roles.includes('admin')
|
||||||
const project = userProject?.Project
|
const project = userProject?.Project
|
||||||
|
|
||||||
const emotes = countEmotes(project?.Reaction)
|
const emotes = countEmotes(project?.Reaction)
|
||||||
const userEmotes = project?.userReactions.map(({ emote }) => emote)
|
const userEmotes = project?.userReactions.map(({ emote }) => emote)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,7 +51,7 @@ const ProjectProfile = ({
|
|||||||
projectTitle: project?.title,
|
projectTitle: project?.title,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}, [currentUser])
|
}, [currentUser, project?.title, userProject.userName])
|
||||||
useIdeInit(project?.cadPackage, project?.code, 'viewer')
|
useIdeInit(project?.cadPackage, project?.code, 'viewer')
|
||||||
const [newDescription, setNewDescription] = useState(project?.description)
|
const [newDescription, setNewDescription] = useState(project?.description)
|
||||||
const onDescriptionChange = (description) => setNewDescription(description())
|
const onDescriptionChange = (description) => setNewDescription(description())
|
||||||
@@ -69,14 +71,9 @@ const ProjectProfile = ({
|
|||||||
<>
|
<>
|
||||||
<div className="h-screen flex flex-col text-lg font-fira-sans">
|
<div className="h-screen flex flex-col text-lg font-fira-sans">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<IdeHeader
|
<TopNav>
|
||||||
handleRender={() => {}}
|
<IdeHeader context="profile" />
|
||||||
projectTitle={project?.title}
|
</TopNav>
|
||||||
projectOwner={userProject?.userName}
|
|
||||||
projectOwnerImage={userProject?.image}
|
|
||||||
projectOwnerId={userProject?.id}
|
|
||||||
projectId={project?.id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex-grow h-full">
|
<div className="relative flex-grow h-full">
|
||||||
<div className="grid grid-cols-1 md:auto-cols-preview-layout grid-flow-row-dense absolute inset-0 h-full">
|
<div className="grid grid-cols-1 md:auto-cols-preview-layout grid-flow-row-dense absolute inset-0 h-full">
|
||||||
@@ -145,6 +142,20 @@ const ProjectProfile = ({
|
|||||||
<KeyValue keyName="Updated on">
|
<KeyValue keyName="Updated on">
|
||||||
{new Date(project?.updatedAt).toDateString()}
|
{new Date(project?.updatedAt).toDateString()}
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
|
{project.forkedFrom && (
|
||||||
|
<KeyValue keyName="Forked from">
|
||||||
|
<Link
|
||||||
|
className="pink-link"
|
||||||
|
to={routes.project({
|
||||||
|
userName: project.forkedFrom.user.userName,
|
||||||
|
projectTitle: project.forkedFrom.title,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{project.forkedFrom.title}
|
||||||
|
</Link>{' '}
|
||||||
|
by {project.forkedFrom.user.userName}
|
||||||
|
</KeyValue>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<KeyValue keyName="Reactions">
|
<KeyValue keyName="Reactions">
|
||||||
<EmojiReaction
|
<EmojiReaction
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ const ProjectsList = ({
|
|||||||
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}
|
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}
|
||||||
>
|
>
|
||||||
{filteredProjects.map(
|
{filteredProjects.map(
|
||||||
({ title, mainImage, user, Reaction, cadPackage }, index) => (
|
(
|
||||||
|
{ title, mainImage, user, Reaction, cadPackage, childForks },
|
||||||
|
index
|
||||||
|
) => (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={index}
|
key={index}
|
||||||
title={title}
|
title={title}
|
||||||
@@ -48,6 +51,7 @@ const ProjectsList = ({
|
|||||||
user={user}
|
user={user}
|
||||||
Reaction={Reaction}
|
Reaction={Reaction}
|
||||||
cadPackage={cadPackage}
|
cadPackage={cadPackage}
|
||||||
|
childForks={childForks}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const QUERY = gql`
|
|||||||
title
|
title
|
||||||
cadPackage
|
cadPackage
|
||||||
mainImage
|
mainImage
|
||||||
|
childForks {
|
||||||
|
id
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
user {
|
user {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const QUERY = gql`
|
|||||||
title
|
title
|
||||||
mainImage
|
mainImage
|
||||||
cadPackage
|
cadPackage
|
||||||
|
childForks {
|
||||||
|
id
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
user {
|
user {
|
||||||
|
|||||||
31
app/web/src/components/TopNav/TopNav.tsx
Normal file
31
app/web/src/components/TopNav/TopNav.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import Svg from 'src/components/Svg/Svg'
|
||||||
|
import NavPlusButton from 'src/components/NavPlusButton'
|
||||||
|
import ProfileSlashLogin from 'src/components/ProfileSlashLogin'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
interface IdeHeaderProps {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopNav = ({ children }: IdeHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<div className="h-16 w-full bg-ch-gray-900 flex justify-between items-center text-lg">
|
||||||
|
<div className="h-full text-gray-300 flex items-center">
|
||||||
|
<div className="w-14 h-16 flex items-center justify-center bg-ch-gray-900">
|
||||||
|
<Link to={routes.home()}>
|
||||||
|
<Svg className="w-12 p-0.5" name="favicon" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
|
||||||
|
<div className="h-8 w-8">
|
||||||
|
<NavPlusButton />
|
||||||
|
</div>
|
||||||
|
<ProfileSlashLogin />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopNav
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useReducer } from 'react'
|
import { useEffect, useReducer } from 'react'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
import ProjectsOfUser from 'src/components/ProjectsOfUserCell'
|
import ProjectsOfUser from 'src/components/ProjectsOfUserCell'
|
||||||
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
import TopNav from 'src/components/TopNav/TopNav'
|
||||||
import Svg from 'src/components/Svg/Svg'
|
|
||||||
import {
|
import {
|
||||||
fieldComponents,
|
fieldComponents,
|
||||||
fieldReducer,
|
fieldReducer,
|
||||||
@@ -27,13 +26,7 @@ function buildFieldsConfig(fieldsConfig, user, hasPermissionToEdit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserProfile = ({
|
const UserProfile = ({ user, isEditing, onSave }: UserProfileType) => {
|
||||||
user,
|
|
||||||
isEditing,
|
|
||||||
loading,
|
|
||||||
onSave,
|
|
||||||
error,
|
|
||||||
}: UserProfileType) => {
|
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const hasPermissionToEdit = currentUser?.sub === user.id
|
const hasPermissionToEdit = currentUser?.sub === user.id
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,14 +53,7 @@ const UserProfile = ({
|
|||||||
<>
|
<>
|
||||||
<div className="md:h-screen flex flex-col text-lg font-fira-sans">
|
<div className="md:h-screen flex flex-col text-lg font-fira-sans">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<IdeHeader
|
<TopNav />
|
||||||
handleRender={() => {}}
|
|
||||||
projectOwner={user?.userName}
|
|
||||||
projectOwnerImage={user?.image}
|
|
||||||
projectOwnerId={user?.id}
|
|
||||||
>
|
|
||||||
<span></span>
|
|
||||||
</IdeHeader>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex-grow h-full">
|
<div className="relative flex-grow h-full">
|
||||||
<div className="grid md:grid-cols-profile-layout grid-flow-row-dense absolute inset-0">
|
<div className="grid md:grid-cols-profile-layout grid-flow-row-dense absolute inset-0">
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
.markdown-overrides div {
|
.markdown-overrides div {
|
||||||
@apply text-ch-gray-300 bg-transparent;
|
@apply text-ch-gray-300 bg-transparent;
|
||||||
}
|
}
|
||||||
|
.pink-link,
|
||||||
.markdown-overrides a,
|
.markdown-overrides a,
|
||||||
.markdown-overrides div a {
|
.markdown-overrides div a {
|
||||||
@apply text-ch-pink-500 underline bg-transparent;
|
@apply text-ch-pink-500 underline bg-transparent;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Toaster } from '@redwoodjs/web/toast'
|
|||||||
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||||
import type { Project } from 'src/components/IdeProjectCell/IdeProjectCell'
|
import type { Project } from 'src/components/IdeProjectCell/IdeProjectCell'
|
||||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||||
|
import type { CadPackageType } from 'src/components/CadPackage/CadPackage'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cadPackage: string
|
cadPackage: string
|
||||||
@@ -21,7 +22,9 @@ const DevIdePage = ({ cadPackage, project }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<Toaster timeout={9000} />
|
<Toaster timeout={9000} />
|
||||||
<IdeContext.Provider value={{ state, thunkDispatch, project }}>
|
<IdeContext.Provider value={{ state, thunkDispatch, project }}>
|
||||||
<IdeWrapper cadPackage={cadPackage.toLocaleLowerCase()} />
|
<IdeWrapper
|
||||||
|
cadPackage={cadPackage.toLocaleLowerCase() as CadPackageType}
|
||||||
|
/>
|
||||||
</IdeContext.Provider>
|
</IdeContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ import { useAuth } from '@redwoodjs/auth'
|
|||||||
|
|
||||||
import ProjectCell from 'src/components/ProjectCell'
|
import ProjectCell from 'src/components/ProjectCell'
|
||||||
import Seo from 'src/components/Seo/Seo'
|
import Seo from 'src/components/Seo/Seo'
|
||||||
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
|
||||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
|
||||||
import { Toaster } from '@redwoodjs/web/toast'
|
import { Toaster } from '@redwoodjs/web/toast'
|
||||||
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages'
|
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages'
|
||||||
|
|
||||||
const ProjectPage = ({ userName, projectTitle }) => {
|
const ProjectPage = ({ userName, projectTitle }) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const [state, thunkDispatch] = useIdeState()
|
|
||||||
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
|
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
|
||||||
userName,
|
userName,
|
||||||
projectTitle
|
projectTitle
|
||||||
@@ -23,13 +20,11 @@ const ProjectPage = ({ userName, projectTitle }) => {
|
|||||||
lang="en-US"
|
lang="en-US"
|
||||||
/>
|
/>
|
||||||
<Toaster timeout={1500} />
|
<Toaster timeout={1500} />
|
||||||
<IdeContext.Provider value={{ state, thunkDispatch, project: null }}>
|
<ProjectCell
|
||||||
<ProjectCell
|
userName={userName}
|
||||||
userName={userName}
|
projectTitle={projectTitle}
|
||||||
projectTitle={projectTitle}
|
currentUserId={currentUser?.sub}
|
||||||
currentUserId={currentUser?.sub}
|
/>
|
||||||
/>
|
|
||||||
</IdeContext.Provider>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user