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:
Frank Noirot
2021-09-28 08:51:36 -04:00
committed by GitHub
17 changed files with 351 additions and 252 deletions

View File

@@ -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;

View File

@@ -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])
} }

View File

@@ -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!

View File

@@ -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>>) =>

View File

@@ -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>
</> </>
) )
} }

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
) )
} }

View File

@@ -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

View File

@@ -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}
/> />
) )
)} )}

View File

@@ -9,6 +9,9 @@ export const QUERY = gql`
title title
cadPackage cadPackage
mainImage mainImage
childForks {
id
}
createdAt createdAt
updatedAt updatedAt
user { user {

View File

@@ -9,6 +9,9 @@ export const QUERY = gql`
title title
mainImage mainImage
cadPackage cadPackage
childForks {
id
}
createdAt createdAt
updatedAt updatedAt
user { user {

View 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

View File

@@ -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">

View File

@@ -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;

View File

@@ -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>
) )

View File

@@ -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>
</> </>
) )
} }