diff --git a/.github/workflows/web4-deploy-production.yml b/.github/workflows/web4-deploy-production.yml index 6256051e..60c41176 100644 --- a/.github/workflows/web4-deploy-production.yml +++ b/.github/workflows/web4-deploy-production.yml @@ -1,4 +1,4 @@ -name: Deploy to production +name: Deploy web4 contract to production on: push: branches: [main] diff --git a/.github/workflows/web4-deploy-staging.yml b/.github/workflows/web4-deploy-staging.yml index 58a70249..9e03aba5 100644 --- a/.github/workflows/web4-deploy-staging.yml +++ b/.github/workflows/web4-deploy-staging.yml @@ -1,4 +1,4 @@ -name: Deploy to staging +name: Deploy web4 contract to staging on: pull_request: paths: diff --git a/.github/workflows/web4-test.yml b/.github/workflows/web4-test.yml index da1a9906..a2d5d479 100644 --- a/.github/workflows/web4-test.yml +++ b/.github/workflows/web4-test.yml @@ -1,4 +1,4 @@ -name: Test +name: Test web4 contract on: workflow_call: diff --git a/.github/workflows/web4-undeploy-staging.yml b/.github/workflows/web4-undeploy-staging.yml index 3b3f862c..bbb252f0 100644 --- a/.github/workflows/web4-undeploy-staging.yml +++ b/.github/workflows/web4-undeploy-staging.yml @@ -1,4 +1,4 @@ -name: Undeploy staging +name: Undeploy web4 contract from staging on: pull_request: types: [closed] diff --git a/apps/new/widget/lib/projects.jsx b/apps/new/widget/lib/projects.jsx index 13ba7bfa..4b9b4963 100644 --- a/apps/new/widget/lib/projects.jsx +++ b/apps/new/widget/lib/projects.jsx @@ -103,8 +103,30 @@ const getProjectIdFromPath = (id) => { return (id ?? "").split("/")[2] ?? null; }; +const fetchCatalogProjects = () => { + const indexer = "https://nearcatalog.xyz/wp-json/nearcatalog/v1"; + return asyncFetch(indexer + "/projects").then((response) => { + if (!response.body) { + return {}; + } + return response.body; + }); +}; + +const fetchCatalogProject = (id) => { + const indexer = "https://nearcatalog.xyz/wp-json/nearcatalog/v1"; + const query = ""; + query = fetch(indexer + "/project?pid=" + id); + if (!query.body) { + return {}; + } + return query.body.profile; +}; + return { fetchProjects, + fetchCatalogProjects, + fetchCatalogProject, getProjectMeta, getProjectIdFromPath, getTagsInArray, diff --git a/apps/new/widget/page/projects/CardSkeleton.jsx b/apps/new/widget/page/projects/CardSkeleton.jsx new file mode 100644 index 00000000..290c2f66 --- /dev/null +++ b/apps/new/widget/page/projects/CardSkeleton.jsx @@ -0,0 +1,125 @@ +const CardSkeletonContainer = styled.div` + @keyframes loadingSkeleton { + 0% { + opacity: 0.8; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 0.6; + } + } + + display: flex; + flex-direction: column; + height: 447px; + width: 100%; + max-width: 400px; + border-radius: 12px; + background: var(--bg-color, #23242b); + color: var(--text-color, #fff); + margin-left: auto; + margin-right: auto; + overflow: hidden; + animation-name: loadingSkeleton; + animation-duration: 1s; + animation-iteration-count: infinite; +`; + +const HeaderSkeleton = styled.div` + display: block; + width: 100%; + height: 168px; + background: #eee; +`; + +const ProfileImageSkeleton = styled.div` + background: #e0e0e0; + margin-left: 32px; + transform: translateY(148px); + width: 40px; + height: 40px; + position: absolute; + border-radius: 999px; +`; + +const TitleSkeleton = styled.div` + width: 120px; + height: 24px; + background: #eee; + margin-left: 24px; + margin-top: 24px; +`; + +const DescriptionSkeleton = styled.div` + width: 83%; + height: 48px; + background: #eee; + margin-left: 24px; + margin-top: 24px; +`; + +const TagSkeleton = styled.div` + background: #eee; + border-radius: 4px; + height: 34px; + width: 110px; + margin: 24px; +`; + +const FooterItemSkeleton = styled.div` + width: 150px; + height: 40px; + background: #eee; + + @media screen and (max-width: 390px) { + width: 100px; + } +`; + +const DonationsInfoContainerSkeleton = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + width: 100%; + border-top: 1px #f0f0f0 solid; +`; + +const DonationsInfoItemSkeleton = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; +`; + +const CardSkeleton = ({ variant }) => { + return varaint === "potlock" ? ( + + + + + + + + + + + + + + + + ) : ( + + + + + + + ); +}; + +return { CardSkeleton }; diff --git a/apps/new/widget/page/projects/CatalogImport.jsx b/apps/new/widget/page/projects/CatalogImport.jsx new file mode 100644 index 00000000..c30889eb --- /dev/null +++ b/apps/new/widget/page/projects/CatalogImport.jsx @@ -0,0 +1,139 @@ +const { Feed } = VM.require("${alias_devs}/widget/Feed") || { + Feed: () => <>, +}; + +const { CardSkeleton } = VM.require( + "${alias_new}/widget/page.projects.CardSkeleton", +) || { + CardSkeleton: () => <>, +}; + +const { fetchCatalogProjects } = VM.require( + "${alias_new}/widget/lib.projects", +) || { + fetchCatalogProjects: () => {}, +}; + +const projects = useCache(() => { + return fetchCatalogProjects(); +}, ["near-catalog-projects"]); + +const [filteredProjects, setFilteredProjects] = useState({}); + +useEffect(() => { + if (projects) { + setFilteredProjects(projects); + } +}, [projects]); + +const [searchTerm, setSearch] = useState(null); + +const Search = useMemo(() => { + return ( + { + setSearch(value); + + if (searchTerm === value) return; + if (searchTerm === "") return; + + const filtered = {}; + Object.keys(projects).forEach((projectId) => { + if ( + projects[projectId].profile.name + .toLowerCase() + .includes(value.toLowerCase()) + ) { + filtered[projectId] = projects[projectId]; + } + + if ( + projects[projectId].profile.tagline + .toLowerCase() + .includes(value.toLowerCase()) + ) { + filtered[projectId] = projects[projectId]; + } + + const tags = Object.values(projects[projectId].profile.tags); + if ( + tags.some((tag) => + tag.toLowerCase().includes(value.toLowerCase()), + ) + ) { + filtered[projectId] = projects[projectId]; + } + }); + setFilteredProjects(filtered); + }, + }} + /> + ); +}, [projects, searchTerm]); + +const ProjectsGrid = styled.div` + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.5rem; + @media screen and (max-width: 768px) { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + max-width: 100%; +`; + +const [selectedProjectId, setSelectedProjectId] = useState(null); + +if (selectedProjectId) { + return ( + + ); +} + +return ( +
+ {Search} + {projects === null ? ( + <> +
+ Loading projects... +
+ + ) : ( + <> + {Object.keys(filteredProjects).length === 0 && ( +
+ {searchTerm + ? `No projects were found for your search query "${searchTerm}".` + : "Network issue: Couldn't fetch any projects, please try again later."} +
+ )} + + )} + ({ projectId }))} + Item={({ projectId }) => ( + } + props={{ + project: filteredProjects[projectId], + setSelectedProjectId: setSelectedProjectId, + }} + /> + )} + Layout={ProjectsGrid} + /> +
+); diff --git a/apps/new/widget/page/projects/CatalogProjectCard.jsx b/apps/new/widget/page/projects/CatalogProjectCard.jsx new file mode 100644 index 00000000..e26b3477 --- /dev/null +++ b/apps/new/widget/page/projects/CatalogProjectCard.jsx @@ -0,0 +1,102 @@ +const project = props.project; +if (!project) { + return "No Project Passed"; +} + +const MAX_DESCRIPTION_LENGTH = 80; + +const Card = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + border-radius: 1rem; + background: #23242b; + color: white; + transition: all 300ms; + height: 100%; + + &:hover { + transform: translateY(-0.5rem); + cursor: pointer; + } + + .image { + height: 168px; + border-radius: 16px 16px 0px 0px; + object-fit: cover; + } + + .info { + display: flex; + flex-direction: column; + padding: 16px 24px; + gap: 16px; + flex: 1; + } + + .title { + font-size: 16px; + font-weight: 600; + width: 100%; + } + + .description { + font-size: 14px; + font-weight: 400; + word-wrap: break-word; + } + + .tags { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .tag { + box-shadow: 0px -0.699999988079071px 0px rgba(123, 123, 123, 0.36) inset; + padding: 4px 8px; + border-radius: 4px; + border: 1px solid rgba(123, 123, 123, 0.36); + } +`; + +const getImageSrc = (image) => { + const defaultImageUrl = + "https://ipfs.near.social/ipfs/bafkreih4i6kftb34wpdzcuvgafozxz6tk6u4f5kcr2gwvtvxikvwriteci"; + if (!image) return defaultImageUrl; + const { url, ipfs_cid } = image; + if (ipfs_cid) { + return ipfsUrlFromCid(ipfs_cid); + } else if (url) { + return url; + } + return defaultImageUrl; +}; + +return ( + props.setSelectedProjectId(project.slug)} + > + {project.profile.name} +
+
{project.profile.name}
+

+ {project.profile.tagline.length > MAX_DESCRIPTION_LENGTH + ? project.profile.tagline.slice(0, MAX_DESCRIPTION_LENGTH) + "..." + : project.profile.tagline} +

+
+ {Object.values(project.profile.tags).map((tag, index) => ( + + {tag} + + ))} +
+
+
+); diff --git a/apps/new/widget/page/projects/Editor.jsx b/apps/new/widget/page/projects/Editor.jsx index 4244812a..7958af39 100644 --- a/apps/new/widget/page/projects/Editor.jsx +++ b/apps/new/widget/page/projects/Editor.jsx @@ -30,6 +30,12 @@ const { getProjectMeta, getProjectIdFromPath } = VM.require( getProjectMeta: () => {}, }; +const { fetchCatalogProject } = VM.require( + "${alias_new}/widget/lib.projects", +) || { + fetchCatalogProject: () => {}, +}; + const { href } = VM.require("${alias_old}/widget/lib.url") || { href: () => {}, }; @@ -271,6 +277,33 @@ const handleTags = (tags) => { setTags(filtered); }; +// Handle NEAR Catalog Projects +const catalogProjectId = props.catalogProjectId; +const catalogProjectData = null; +if (catalogProjectId) { + catalogProjectData = fetchCatalogProject(catalogProjectId); +} +useEffect(() => { + if (catalogProjectData) { + const { website, github, telegram, twitter } = catalogProjectData.linktree; + const githubLink = github.split("/")[3]; + const telegramLink = telegram.split("/")[3]; + const twitterLink = twitter.split("/")[3]; + const tags = Object.values(catalogProjectData.tags || []).map((tag) => { + return removeWhiteSpace(tag); + }); + + setTitle(catalogProjectData.name); + setDescription(catalogProjectData.description); + setAvatar(catalogProjectData.image); + setWebsite(website); + setGitHub(githubLink); + setTelegram(telegramLink); + setTwitter(twitterLink); + setTags(tags); + } +}, [catalogProjectData]); + // Commenting roles code (to be added in v1) // const handleRoles = (roles) => { // let filtered = roles.map((role) => diff --git a/apps/new/widget/page/projects/ImportAndCreateModal.jsx b/apps/new/widget/page/projects/ImportAndCreateModal.jsx index 25884f4d..74b3d738 100644 --- a/apps/new/widget/page/projects/ImportAndCreateModal.jsx +++ b/apps/new/widget/page/projects/ImportAndCreateModal.jsx @@ -66,14 +66,18 @@ return ( src="https://ipfs.near.social/ipfs/bafkreidbfu7uxtr4is7wxileg3mrbajve6cgkfmrqemc6pxsr6nnczz7ly" tab="editor" /> - - +