created initial marketplace with linking to details

This commit is contained in:
philip-ellis-sp
2023-05-31 16:08:11 -05:00
parent de4a97782e
commit 1ab2ae00fe
15 changed files with 838 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
export default function MarketplaceBanner() {
return (
<div>
<div className={styles.imageContainer}>
<div className={styles.blogHeaderText}>
SailPoint Developer Marketplace
</div>
</div >
</div>
);
}

View File

@@ -0,0 +1,24 @@
.blogHeaderText {
position: relative;
color: var(--ifm-color-primary);
font-size: 30px;
max-width: 396px;
font-weight: bold;
line-height: 100%;
top: 20px;
left: 20px;
}
.background {
width: 100%;
object-fit: repeat;
height: 100%;
}
.imageContainer {
width: 100%;
height: 180px;
background-image: url('../../../../static/blog/blog_banner_template.png');
background-repeat: repeat;
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import styles from './styles.module.css';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import {addDarkToFileName} from '../../../util/util';
export default function MarketplaceCard({
link,
title,
tags,
creatorImage,
image,
excerpt,
name,
views,
replies,
readTime,
openDialogFunc,
rawData
}) {
function setFilters(e) {
openDialogFunc(rawData);
}
return (
<div onClick={(e) => setFilters(e)}>
<div className={styles.card} >
<div className={styles.cardData}>
<img className={styles.cardEye} src={useBaseUrl('/blog/eye-regular.svg')}></img>
<div className={styles.cardCommentText}>{views}</div>
<img className={styles.cardComment} src={useBaseUrl('/blog/comment-light.svg')}></img>
<div className={styles.cardCommentText}>{replies}</div>
<img className={styles.cardComment} src={useBaseUrl('/blog/clock-light.svg')}></img>
<div className={styles.cardCommentText}>{readTime} minute read</div>
</div>
<div className={styles.cardUser}>
<img className={styles.cardFace} src={useBaseUrl(creatorImage)}></img>
<div className={styles.cardName}>{name}</div>
</div>
<div className={styles.cardText}>
<img className={styles.cardImage} src={useBaseUrl(image)}></img>
<div className={styles.cardTitle}>{title}</div>
<div className={styles.tags}>
{tags?.map((tag, index) => {
if (index > 2) {
return '';
}
return <div key={tag} className={styles.tag}>{tag}</div>;
})}
</div>
<div className={styles.cardBody}>{excerpt}</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,127 @@
/* Getting Started Card */
.card {
position: relative;
margin-top: 20px;
height: 650px;
/* UI Properties */
background: var(--dev-card-background);
box-shadow: var(--dev-card-shadow);
border: 1px solid var(--dev-card-background);
border-radius: 40px;
opacity: 1;
transition: all 0.3s;
max-width: 600px;
}
.card:hover {
cursor: pointer;
transform: translate(0px, -5px);
box-shadow: var(--dev-card-selected);
}
.cardText {
position: absolute;
margin: 22px;
min-width: 170px;
top: 10px;
left: 0;
}
.cardTitle {
font-size: 22px;
font-weight: 700;
}
.cardBody {
font-size: 16px;
font-weight: 500;
top: 10px;
left: 0;
}
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
background-color: var(--dev-tag-highlight);
padding: 0px 8px;
margin-left: 5px;
margin-top: 5px;
}
.tags {
margin: 0px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.cardUser {
position: absolute;
bottom: 10px;
left: 25px;
display: flex;
}
.cardFace {
border-radius: 9999px;
margin: auto;
height: 40px;
width: 40px;
}
.cardName {
min-width: 170px;
margin-top: 7px;
margin-left: 10px;
}
.cardData {
position: absolute;
bottom: 50px;
left: 20px;
display: flex;
align-items: center;
}
.cardEye {
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 18px;
width: 18px;
}
.cardComment {
margin-left: 10px;
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
height: 18px;
width: 18px;
margin-bottom: 4px;
}
.cardCommentText {
color: #96a9ba;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
margin-left: 5px;
margin-bottom: 5px;
height: 20px;
font-size: 16px;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import styles from './styles.module.css';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import {addDarkToFileName} from '../../../util/util';
export default function MarketplaceCardDetail({
details
}) {
return (
<div onClick={(e) => setFilters(e)}>
<div>{details}</div>
</div>
);
}

View File

@@ -0,0 +1,127 @@
/* Getting Started Card */
.card {
position: relative;
margin-top: 20px;
height: 650px;
/* UI Properties */
background: var(--dev-card-background);
box-shadow: var(--dev-card-shadow);
border: 1px solid var(--dev-card-background);
border-radius: 40px;
opacity: 1;
transition: all 0.3s;
max-width: 600px;
}
.card:hover {
cursor: pointer;
transform: translate(0px, -5px);
box-shadow: var(--dev-card-selected);
}
.cardText {
position: absolute;
margin: 22px;
min-width: 170px;
top: 10px;
left: 0;
}
.cardTitle {
font-size: 22px;
font-weight: 700;
}
.cardBody {
font-size: 16px;
font-weight: 500;
top: 10px;
left: 0;
}
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
background-color: var(--dev-tag-highlight);
padding: 0px 8px;
margin-left: 5px;
margin-top: 5px;
}
.tags {
margin: 0px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.cardUser {
position: absolute;
bottom: 10px;
left: 25px;
display: flex;
}
.cardFace {
border-radius: 9999px;
margin: auto;
height: 40px;
width: 40px;
}
.cardName {
min-width: 170px;
margin-top: 7px;
margin-left: 10px;
}
.cardData {
position: absolute;
bottom: 50px;
left: 20px;
display: flex;
align-items: center;
}
.cardEye {
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 18px;
width: 18px;
}
.cardComment {
margin-left: 10px;
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
height: 18px;
width: 18px;
margin-bottom: 4px;
}
.cardCommentText {
color: #96a9ba;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
margin-left: 5px;
margin-bottom: 5px;
height: 20px;
font-size: 16px;
}

View File

@@ -0,0 +1,129 @@
import React from 'react';
import styles from './styles.module.css';
import MarketplaceCard from '../MarketplaceCard';
import Modal from 'react-modal';
import {getMarketplacePosts, getMarketplaceTopic, getMarketplaceTopicRaw} from '../../../services/DiscourseService';
import MarketplaceCardDetail from '../MarketplaceCardDetail';
export default function BlogCards({
filterCallback
}) {
const [cardData, setCardData] = React.useState();
const [detailsOpen, setDetailsOpen] = React.useState(false);
const [details, setDetails] = React.useState("");
const getPosts = async () => {
const data = await getMarketplacePosts(filterCallback.join(','));
const resultset = []
if (data.topics) {
for (const topic of data.topics) {
resultset.push(await getPostList(topic))
}
setCardData(resultset);
} else {
setCardData(undefined);
}
};
const openDialog = (data) => {
setDetails(data);
setDetailsOpen(true);
}
Modal.setAppElement('#__docusaurus');
React.useEffect(() => {
getPosts();
}, [filterCallback]);
if (cardData) {
return (
<div className={styles.center}>
<div className={styles.gridContainer}>
{cardData.map(function(a, index){
return <MarketplaceCard
key={a.link}
id={index + a.link}
excerpt={a.excerpt}
name={a.name}
tags={a.tags}
link={a.link}
image={a.image}
title={a.title}
views={a.views}
replies={a.replies}
readTime={a.readTime}
creatorImage={a.creatorImage}
openDialogFunc={openDialog}
rawData={a.raw}
></MarketplaceCard>
})}
</div>
<Modal
isOpen={detailsOpen}
className={styles.modal}
contentLabel="Details">
<div>
Modal Works!
<MarketplaceCardDetail details={details}></MarketplaceCardDetail>
</div>
<button
className={styles.modalButton}
onClick={async () => {
setDetailsOpen(false);
}}>
OK
</button>
</Modal>
</div>
);
} else {
return <div className={styles.noFound}> No Marketplace Item Found with the Given Search Criteria</div>;
}
}
async function getPostList(topic) {
console.log(topic)
const fullTopic = await getMarketplaceTopic(topic.id);
const fullTopicRaw = await getMarketplaceTopicRaw(topic.id);
console.log(fullTopic)
console.log(fullTopicRaw)
return {
raw: fullTopicRaw,
name: fullTopic.details.created_by.name,
excerpt: styleExcerpt(topic.excerpt),
creatorImage: getavatarURL(fullTopic.details.created_by.avatar_template),
tags: topic.tags,
image: fullTopic.image_url,
link:
'https://developer.sailpoint.com/discuss/t/' +
topic.slug +
'/' +
topic.id,
title: topic.title,
views: fullTopic.views,
liked: topic.like_count,
replies: fullTopic.posts_count,
solution: topic.has_accepted_answer,
readTime: parseInt(fullTopic.word_count/100),
};
}
function getavatarURL(avatar) {
return "https://developer.sailpoint.com" + avatar.replace("{size}", "120")
}
function styleExcerpt(excerpt) {
if (excerpt) {
if (excerpt.length > 150) {
return excerpt.slice(0, 150) + "..."
} else {
return excerpt.replace("&hellip;", "")
}
} else {
return ""
}
}

View File

@@ -0,0 +1,66 @@
/* Getting Started Card container */
.gridContainer {
display: grid;
place-content: center;
grid-template-columns: repeat(auto-fit, minmax(345px, 1fr));
grid-gap: 40px;
margin-left: 40px;
margin-right: 40px;
}
.center {
margin: 0px auto;
max-width: 1300px;
margin-bottom: 50px;
}
.space {
height: 200px;
}
.noFound {
font-size: 28px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin: 50px;
}
.modal {
position: absolute;
margin-top: 50px;
top: 50%;
left: 50%;
right: auto;
bottom: auto;
margin-right: -50%;
transform: translate(-50%, -50%);
box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.25);
border-radius: 20px;
background-color: var(--dev-card-background);
max-height: 100%;
}
.modalButton {
border: 1px solid var(--ifm-color-primary);
box-shadow: 1px 4px 10px rgba(0, 0, 0, 0.12);
border-radius: 24px;
color: var(--ifm-color-primary);
margin: 10px;
padding: 10px;
text-align: center;
width: 120px;
background-color: #ffffff31;
}
.modalButton:hover {
cursor: pointer;
top: -4px;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.4);
background-color: #dae1e9;
color: #005fc4;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import styles from './styles.module.css';
export default function BlogSidebarButton({
filterCallback,
text
}) {
const [isActive, setIsActive] = React.useState(false);
const activeClass = isActive ? styles.tagSelected : ''
function setFilters(e, f) {
filterCallback(f)
setIsActive(current => !current);
}
return <div key={text} onClick={(e) => setFilters(e, text)} className={activeClass + ' ' + styles.tag}>{text}</div>
}

View File

@@ -0,0 +1,36 @@
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin-right: 5px;
margin-top: 5px;
margin-bottom: 5px;
border-style: solid;
border-width: 1px;
border-color: var(--dev-text-color-normal);
transition: background-color 500ms;
}
.tagSelected {
font-size: 16px;
font-weight: 500;
background-color: var(--dev-secondary-text);
color: var(--dev-card-background);
padding: 8px;
margin-right: 5px;
margin-bottom: 5px;
margin-top: 5px;
border-style: solid;
border-width: 1px;
border-color: var(--dev-text-color-normal);
transition: background-color 500ms;
}
.tag:hover {
cursor: pointer;
background-color: var(--dev-text-color-normal);
color: var(--dev-card-background);
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
import { getTags } from '../../../services/DiscourseService';
import BlogSidebarButton from './BlogSidebarButton';
export default function BlogSidebar({
filterCallback
}) {
const [tagProductData, setTagProductData] = React.useState();
const [tagTechnologyData, setTagTechnologyData] = React.useState();
const [filterTags, setFilterTags] = React.useState(true);
const getTagData = async () => {
const data = await getTags();
const tagTechnologyResultset = []
const tagProductResultset = []
for (const tagGroup of data.extras.tag_groups) {
if (tagGroup.id === 20) {
for (const tag of tagGroup.tags) {
tagProductResultset.push(tag.text)
}
}
if (tagGroup.id === 17) {
for (const tag of tagGroup.tags) {
tagTechnologyResultset.push(tag.text)
}
}
}
setTagProductData(tagProductResultset)
setTagTechnologyData(tagTechnologyResultset)
};
function toggleSeeAll() {
filterTags ? setFilterTags(false) : setFilterTags(true)
}
React.useEffect(() => {
getTagData();
}, []);
const filterText = filterTags ? 'See All Tags' : 'See Less Tags'
if (tagProductData && tagTechnologyData) {
return (
<div className={styles.sidebar}>
<div className={styles.tagHeader}>Posts by Product</div>
<div className={styles.tagContainer}>
{tagProductData.map(function(a, index){
return <BlogSidebarButton key={a} text={a} filterCallback={filterCallback}></BlogSidebarButton>
})}
</div>
<div className={styles.tagHeader}>Posts by Identity Governance</div>
<div className={styles.tagContainer}>
{tagTechnologyData.map(function(a, index){
return <div key={'div' + a} className={index > 10 && filterTags ? styles.hidden : ''} > <BlogSidebarButton key={a} text={a} filterCallback={filterCallback}></BlogSidebarButton></div>
})}
</div>
<div className={styles.seeAll} onClick={(e) => toggleSeeAll()}>
{filterText}
{/* <img className={styles.caretDown} src={useBaseUrl('/blog/caret-down-thin.svg')}></img> */}
</div>
</div>
);
} else {
return <div></div>;
}
}

View File

@@ -0,0 +1,40 @@
.sidebar {
width: 400px;
height: 100%;
margin-left: 50px;
}
.tagHeader {
margin-top: 10px;
font-size: 22px;
font-weight: 700;
}
.tagContainer {
display: flex;
flex-wrap: wrap;
}
.hidden {
display: none;
}
.seeAll {
margin-top: 5px;
margin-bottom: 10px;
color: var(--dev-secondary-text);
border-style: solid;
border-width: 1px;
width: 100%;
transition: background-color 500ms;
text-align: center;
}
.seeAll:hover {
cursor: pointer;
background-color: var(--dev-text-color-normal);
color: var(--dev-card-background);
}

41
src/pages/marketplace.js Normal file
View File

@@ -0,0 +1,41 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import MarketplaceBanner from '../components/marketplace/MarketplaceBanner';
import styles from './marketplace.module.css';
import MarketplaceCards from '../components/marketplace/MarketplaceCards';
import MarketplaceSidebar from '../components/marketplace/MarketplaceSidebar';
export default function Marketplace() {
const [filteredProduct, setFilteredProduct] = React.useState([]);
const {siteConfig} = useDocusaurusContext();
const handleClick = (data) => {
var tempFilter = filteredProduct.slice()
const index = tempFilter.indexOf(data);
if (index !== -1) {
tempFilter.splice(index, 1);
} else {
tempFilter.push(data)
}
setFilteredProduct(tempFilter)
};
return (
<Layout description="The SailPoint Developer Community has everything you need to build, extend, and automate scalable identity solutions.">
<main>
<MarketplaceBanner />
<div className={styles.blogContainer}>
<div className={styles.blogSidbarContainer}><MarketplaceSidebar filterCallback={handleClick}/></div>
<div className={styles.blogCardContainer}><MarketplaceCards filterCallback={filteredProduct}/></div>
</div>
</main>
</Layout>
);
}

View File

@@ -0,0 +1,17 @@
.blogContainer {
display: flex;
}
.blogSidbarContainer {
flex: 5%;
}
@media only screen and (max-width: 870px) {
.blogSidbarContainer {
display: none;
}
}
.blogCardContainer {
flex: 95%;
}

View File

@@ -26,6 +26,23 @@ export async function getBlogPosts(tags) {
}
}
export async function getMarketplacePosts(tags) {
let url = ''
if (tags) {
url = 'https://developer.identitysoon.com/discuss/search.json?q=category:show-and-tell+tags:' + tags
} else {
url = 'https://developer.identitysoon.com/discuss/search.json?q=category:show-and-tell'
}
try {
const response = await fetch(
url,
);
return await response.json();
} catch (error) {
return [];
}
}
export async function getTopic(id) {
try {
const response = await fetch(
@@ -37,6 +54,28 @@ export async function getTopic(id) {
}
}
export async function getMarketplaceTopic(id) {
try {
const response = await fetch(
'https://developer.identitysoon.com/discuss/t/' + id + '.json',
);
return await response.json();
} catch (error) {
return [];
}
}
export async function getMarketplaceTopicRaw(id) {
try {
const response = await fetch(
'https://developer.identitysoon.com/discuss/raw/' + id + '.json',
);
return await response.text();
} catch (error) {
return [];
}
}
export async function getTags() {
try {
const response = await fetch(