diff --git a/src/AccountProfile.jsx b/src/AccountProfile.jsx index 864f1056..953b631a 100644 --- a/src/AccountProfile.jsx +++ b/src/AccountProfile.jsx @@ -154,4 +154,4 @@ return ( showFlagAccountFeature, }} /> -); +); \ No newline at end of file diff --git a/src/AccountProfileOverlay.jsx b/src/AccountProfileOverlay.jsx index 3f5c58af..02ca3f8a 100644 --- a/src/AccountProfileOverlay.jsx +++ b/src/AccountProfileOverlay.jsx @@ -30,6 +30,13 @@ const onReport = () => { }); }; +const AvatarCount = styled.div` + color: rgb(104, 112, 118); + font-size: 14px; + font-weight: 300; + padding-left: 12px; +`; + const CardWrapper = styled.div` z-index: 100; padding: 6px; @@ -66,6 +73,17 @@ const VerificationText = styled.div` color: ${(props) => (props.secondary ? "#717069" : "black")}; `; +const RecommendedAvatars = styled.div` + display: flex; + flex-direction: row; + align-items: center; + padding: 12px 0 0; +`; + +const OverlayTagsWrapper = styled.div` + margin-left: 50px; +`; + const contentModerationItem = { type: "social", path: profileUrl, @@ -76,10 +94,22 @@ const overlay = (
- + {props.sidebar && ( + + )} + {!props.sidebar && ( + + )} {!props.showFlagAccountFeature && !!context.accountId && context.accountId !== props.accountId && ( @@ -211,16 +241,51 @@ const overlay = (
)} - - {!!context.accountId && context.accountId !== props.accountId && ( - + {props.scope === "friends" ? ( + + + + + {props.becauseYouFollow.length} friends following + + + + ) : ( + + )} + + {!!context.accountId && context.accountId !== props.accountId && ( + + {props.sidebar && ( + + )} + {!props.sidebar && ( + + )} )}
@@ -275,4 +340,4 @@ return ( }} /> -); +); \ No newline at end of file diff --git a/src/FollowButton.jsx b/src/FollowButton.jsx index 1e3e7b8a..ad1222aa 100644 --- a/src/FollowButton.jsx +++ b/src/FollowButton.jsx @@ -61,20 +61,20 @@ const Wrapper = styled.div` line-height: 15px; text-align: center; cursor: pointer; - background: #FBFCFD; - border: 1px solid #D7DBDF; - color: #006ADC !important; + background: #fbfcfd; + border: 1px solid #d7dbdf; + color: #006adc !important; white-space: nowrap; &:hover, &:focus { - background: #ECEDEE; + background: #ecedee; text-decoration: none; outline: none; } i { - color: #7E868C; + color: #7e868c; } .bi-16 { @@ -85,7 +85,17 @@ const Wrapper = styled.div` return ( - + { + props.onCommit(); + }} + onClick={() => { + props.onClick(); + }} + disabled={loading} + className="follow-button" + data={data} + > {isFollowing && } {isFollowing ? "Following" : isInverse ? "Follow Back" : "Follow"} diff --git a/src/LatestPeople.jsx b/src/LatestPeople.jsx index e2e51f72..05e14435 100644 --- a/src/LatestPeople.jsx +++ b/src/LatestPeople.jsx @@ -23,6 +23,28 @@ let accounts = Object.entries(accountsWithProfileData || {}) accounts.reverse(); +State.init({ + selectedView: { text: "Top", value: "trending" }, +}); + +const handleViewChange = (option) => { + State.update({ selectedView: { value: option.value } }); +}; + +const options = [ + { text: "Latest", value: "latest" }, + { text: "Top", value: "trending" }, + { text: "Recommended", value: "recommended" }, +]; + +const fromContext = props; + +const FlexContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + const Wrapper = styled.div` display: grid; gap: 24px; @@ -35,7 +57,7 @@ const H2 = styled.h2` margin: 0; `; -const Items = styled.div` +const LatestPeople = styled.div` display: grid; gap: 18px; `; @@ -70,26 +92,72 @@ const ButtonLink = styled.a` } `; +const Select = styled.div` + max-width: 100%; +`; + return ( -

People

- - - {accounts.map((account) => ( - - - - ))} - - - - View All People ({totalAccounts}) - + +

People

+ +
+ {state.selectedView.value === "latest" && ( + <> + + {accounts.map((account) => ( + + + + ))} + + + View All People ({totalAccounts}) + + + )} + {state.selectedView.value === "trending" && ( + <> + + + )} + {state.selectedView.value === "recommended" && ( + <> + + + )}
); diff --git a/src/PeoplePage.jsx b/src/PeoplePage.jsx index cadfb420..302aa4e1 100644 --- a/src/PeoplePage.jsx +++ b/src/PeoplePage.jsx @@ -3,6 +3,7 @@ let people = []; const peopleUrl = "#/${REPL_ACCOUNT}/widget/PeoplePage"; let followingData = null; let followersData = null; +const fromContext = props; State.init({ currentPage: 0, @@ -46,7 +47,9 @@ if (data) { if ( state.selectedTab === "everyone" || (state.selectedTab === "following" && isFollowing) || - (state.selectedTab === "followers" && isFollower) + (state.selectedTab === "followers" && isFollower) || + state.selectedTab === "trending" || + state.selectedTab === "recommended" ) { result.push({ accountId, @@ -275,6 +278,20 @@ return ( Followers )} + + Trending + + {context.accountId && ( + + Recommended + + )} )} @@ -282,29 +299,60 @@ return ( No people matched your search. )} - {items.length > 0 && ( - - {items.map((person, i) => ( - - - - ))} - + {(state.selectedTab == "everyone" || + state.selectedTab == "following" || + state.selectedTab == "followers") && + items.length > 0 && ( + + {items.map((person, i) => ( + + + + ))} + + )} + + {!context.accountId && state.selectedTab == "trending" && ( + + )} + + {context.accountId && state.selectedTab == "trending" && ( + )} - {showLoadMoreButton && ( - + {context.accountId && state.selectedTab == "recommended" && ( + )} + + {(state.selectedTab == "everyone" || + state.selectedTab == "following" || + state.selectedTab == "followers") && + showLoadMoreButton && ( + + )}
); diff --git a/src/Recommender/Account/AccountProfileLargeCard.jsx b/src/Recommender/Account/AccountProfileLargeCard.jsx new file mode 100644 index 00000000..99dc2431 --- /dev/null +++ b/src/Recommender/Account/AccountProfileLargeCard.jsx @@ -0,0 +1,245 @@ +const accountId = props.accountId; +const profile = props.profile || Social.get(`${accountId}/profile/**`, "final"); +const tags = Object.keys(profile.tags || {}); +const profileUrl = `#/${REPL_ACCOUNT}/widget/ProfilePage?accountId=${accountId}`; + +const abbreviateNumber = (value) => { + let newValue = value; + if (value >= 1000) { + const suffixes = ["", "k", "M"]; + let suffixNum = 0; + if (value < 1000000) { + suffixNum = 1; // Use 'k' + } else if (value < 1000000000) { + suffixNum = 2; // Use 'M' + } + const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1); + newValue = shortValue + suffixes[suffixNum]; + } + return newValue; +}; + +const Avatar = styled.a` + width: 60px; + height: 60px; + flex-shrink: 0; + border: 1px solid #eceef0; + overflow: hidden; + border-radius: 56px; + transition: border-color 200ms; + + img { + object-fit: cover; + width: 100%; + height: 100%; + } + + &:hover, + &:focus { + border-color: #d0d5dd; + } +`; + +const Button = styled.div` + div > .follow-button { + color: #000 !important; + } +`; + +const CurrentUserProfile = styled.div` + height: 32px; +`; + +const NoTags = styled.div` + height: 13px; +`; + +const Score = styled.li` + font-size: 12px; + font-weight: 500; + color: #90908c; + i { + font-size: 12px; + font-weight: 900; + color: #90908c; + } +`; + +const Scores = styled.ul` + display: flex; + justify-content: space-around; + align-items: left; + width: 100%; + margin-bottom: 0px; + padding: 0px 40px 14px; + border-bottom: 1px solid #eceef0; + color: #90908c; + list-style-type: none; + min-height: 32px; +`; + +const TagsWrapper = styled.div` + max-width: 80%; + margin-top: -5px; +`; + +const ProfileListContainer = styled.div` + width: auto; + position: relative; + font-size: 14px; + color: #90908c; +`; + +const ProfileList = styled.ul` + list-style: none; + padding: 0; + margin: 0; + height: 100%; + font-size: 14px; + color: #90908c; +`; + +const Profile = styled.li` + padding: 0px; +`; + +const FollowsYouBadge = styled.p` + display: inline-block; + margin: 0px; + font-size: 10px; + line-height: 1.1rem; + background: rgb(104, 112, 118); + color: rgb(255, 255, 255); + font-weight: 600; + white-space: nowrap; + padding: 2px 6px; + border-radius: 3px; +`; + +const LargeCard = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: 12px; + width: 100%; + border-radius: 12px; + z-index: 1070; + background: #fff; + border: 1px solid #eceef0; + box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), + 0px 1px 2px rgba(16, 24, 40, 0.06); + overflow: hidden; + padding: 24px 0px 13px; +`; + +return ( + + + + + + + + + + + {tags.length == 0 && ( + + + + )} + + {props.following !== null && + props.followers !== null && + props.likers !== null ? ( + + {props.followers > 0 && ( + + Followers} + > +
+ + {abbreviateNumber(props.followers)} +
+
+
+ )} + {props.following > 0 && ( + + Following} + > +
+ + {abbreviateNumber(props.following)} +
+
+
+ )} + {props.likers > 0 && ( + + Likes received} + > +
+ + {abbreviateNumber(props.likers)} +
+
+
+ )} +
+ ) : ( + + + + )} + + {context.accountId && context.accountId !== props.accountId ? ( + + ) : ( + + )} +
+); diff --git a/src/Recommender/Account/AccountProfileSidebar.jsx b/src/Recommender/Account/AccountProfileSidebar.jsx new file mode 100644 index 00000000..c39b04db --- /dev/null +++ b/src/Recommender/Account/AccountProfileSidebar.jsx @@ -0,0 +1,186 @@ +const accountId = props.accountId || context.accountId; +const profile = props.profile || Social.get(`${accountId}/profile/**`, "final"); +const profileUrl = `#/${REPL_ACCOUNT}/widget/ProfilePage?accountId=${accountId}`; +const verifications = props.verifications; + +const Wrapper = styled.a` + display: inline-grid; + width: 100%; + align-items: center; + gap: 12px; + grid-template-columns: auto 1fr; + cursor: pointer; + margin: 0; + color: #687076 !important; + outline: none; + text-decoration: none !important; + background: none !important; + border: none; + text-align: left; + padding: 0; + + > * { + min-width: 0; + } + + .hover-state1 { + opacity: 0; + } + + .hover-state2 { + opacity: 1; + } + + &:hover, + &:focus { + div.hover { + border-color: #d0d5dd; + } + .hover-state1 { + opacity: 1; + transition: opacity 0.3s ease; + } + .hover-state2 { + opacity: 0; + transition: opacity 0.3s ease; + } + } +`; + +const AvatarCount = styled.div` + color: rgb(104, 112, 118); + font-size: 14px; + font-weight: 300; + position: absolute; + right: 0px; + padding-right: 20px; +`; + +const Text = styled.p` + margin: 0; + font-size: 14px; + line-height: 20px; + color: ${(p) => (p.bold ? "#11181C" : "#687076")}; + font-weight: ${(p) => (p.bold ? "600" : "400")}; + font-size: ${(p) => (p.small ? "10px" : "14px")}; + overflow: ${(p) => (p.ellipsis ? "hidden" : "")}; + text-overflow: ${(p) => (p.ellipsis ? "ellipsis" : "")}; + white-space: nowrap; +`; + +const Avatar = styled.div` + width: ${props.avatarSize || "40px"}; + height: ${props.avatarSize || "40px"}; + flex-shrink: 0; + border: 1px solid #eceef0; + overflow: hidden; + border-radius: 40px; + transition: border-color 200ms; + + img { + object-fit: cover; + width: 100%; + height: 100%; + } +`; + +const VerifiedBadge = styled.div` + position: absolute; + left: 24px; + top: 22px; +`; + +const Name = styled.div` + display: flex; + gap: 6px; + align-items: center; +`; + +const AccountProfile = ( + props.onClick(accountId))} + > + + + + + {verifications && ( + + + + + + + + )} + +
+ + + +
+
+); + +if (props.noOverlay) return AccountProfile; + +return ( + +); diff --git a/src/Recommender/Engagement/CenteredLinksWrapperTracked.jsx b/src/Recommender/Engagement/CenteredLinksWrapperTracked.jsx new file mode 100644 index 00000000..fa68b7d7 --- /dev/null +++ b/src/Recommender/Engagement/CenteredLinksWrapperTracked.jsx @@ -0,0 +1,56 @@ +const handleClick = () => { + State.update({ clicked: true }); +}; + +State.init({ + clicked: false, +}); + +const CenteredLinksWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const TextLink = styled.a` + display: block; + margin: 0; + font-size: 14px; + line-height: 18px; + color: ${(p) => (p.bold ? "#11181C !important" : "#687076 !important")}; + font-weight: ${(p) => (p.bold ? "600" : "400")}; + font-size: ${(p) => (p.small ? "12px" : "14px")}; + overflow: ${(p) => (p.ellipsis ? "hidden" : "visible")}; + text-overflow: ${(p) => (p.ellipsis ? "ellipsis" : "unset")}; + white-space: nowrap; + outline: none; + max-width: 230px; + + &:focus, + &:hover { + text-decoration: underline; + } +`; + +return ( +
+ + + + {props.profileName} + + + @{props.accountId} + + +
+); diff --git a/src/Recommender/Engagement/FollowButtonTracked.jsx b/src/Recommender/Engagement/FollowButtonTracked.jsx new file mode 100644 index 00000000..e7c739f3 --- /dev/null +++ b/src/Recommender/Engagement/FollowButtonTracked.jsx @@ -0,0 +1,30 @@ +State.init({ + clicked: false, +}); + +return ( +
+ + { + props.onFollowed(); + }, + onClick: () => { + State.update({ clicked: true }); + }, + }} + /> +
+); diff --git a/src/Recommender/Engagement/ImageTracked.jsx b/src/Recommender/Engagement/ImageTracked.jsx new file mode 100644 index 00000000..86cca477 --- /dev/null +++ b/src/Recommender/Engagement/ImageTracked.jsx @@ -0,0 +1,31 @@ +const handleClick = () => { + State.update({ clicked: true }); +}; + +State.init({ + clicked: false, +}); + +return ( +
+ + +
+); \ No newline at end of file diff --git a/src/Recommender/Engagement/ProfileInfoTracked.jsx b/src/Recommender/Engagement/ProfileInfoTracked.jsx new file mode 100644 index 00000000..bad19aa6 --- /dev/null +++ b/src/Recommender/Engagement/ProfileInfoTracked.jsx @@ -0,0 +1,126 @@ +const handleClick = () => { + State.update({ clicked: true }); +}; + +State.init({ + clicked: false, +}); + +const Wrapper = styled.a` + text-decoration: none !important; + + .solid-state { + position: absolute; + margin-right: 26px; + right: 0; + } + + .hover-state1 { + position: absolute; + right: 0; + margin-right: 26px; + opacity: 0; + } + + .hover-state2 { + position: absolute; + right: 0; + margin-right: 26px; + opacity: 1; + } + + &:hover, + &:focus { + div.hover { + border-color: #d0d5dd; + } + .hover-state1 { + opacity: 1; + transition: opacity 0.3s ease; + } + .hover-state2 { + opacity: 0; + transition: opacity 0.3s ease; + } + } +`; + +const Text = styled.p` + margin: 0; + font-size: 14px; + line-height: 20px; + color: ${(p) => (p.bold ? "#11181C" : "#687076")}; + font-weight: ${(p) => (p.bold ? "600" : "400")}; + font-size: ${(p) => (p.small ? "10px" : "14px")}; + overflow: ${(p) => (p.ellipsis ? "hidden" : "")}; + text-overflow: ${(p) => (p.ellipsis ? "ellipsis" : "")}; + white-space: nowrap; + width: ${props.scope ? "100px" : "170px"}; +`; + +const AvatarCount = styled.div` + color: rgb(104, 112, 118); + font-size: 14px; + font-weight: 300; + padding-left: 12px; +`; + +const Tracker = styled.div` + display: flex; + flexdirection: column; + justifycontent: space-between; +`; + +return ( + + + + {props.scope == "friends" && props.becauseYouFollow.length > 0 && ( + + + + + + {props.becauseYouFollow.length} friends follow + + + )} + {props.scope == "similar" && ( + + Shared interests + + )} + + + {props.profileName} + + + @{props.accountId} + {props.inlineContent} + {props.blockHeight && ( + + Joined{" "} + {" "} + ago + + )} + +); diff --git a/src/Recommender/Service/EngagementTracker.jsx b/src/Recommender/Service/EngagementTracker.jsx new file mode 100644 index 00000000..8f27e456 --- /dev/null +++ b/src/Recommender/Service/EngagementTracker.jsx @@ -0,0 +1,48 @@ +const dataplane = "https://near.dataplane.rudderstack.com"; //prod +const uri = "/v1/track"; +const api_url = `${dataplane}${uri}`; +const auth = "Basic MlVub3dMd2lXRnc3YzM1QU11RUVkREVJa2RvOg=="; //prod + +const generateAnonId = (length) => { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters.charAt(randomIndex); + } + return result; +}; + +const trackEngagement = () => { + const payload = { + anonymousId: generateAnonId(24), + channel: "web", + context: props.fromContext, + type: "track", + originalTimestamp: new Date().toISOString(), + sentAt: new Date().toISOString(), + event: props.event, + properties: { + accountId: props.accountId, + accountIdRank: props.accountIdRank, + }, + }; + + asyncFetch(api_url, { + body: JSON.stringify(payload), + headers: { + "Content-Type": "application/json", + Authorization: auth, + }, + method: "POST", + }) + .then((response) => response.json()) + .then((data) => console.log(data)) + .catch((error) => console.log(error)); +}; + +props.onClick ? trackEngagement(props) : null; + +return <>{props.children}; diff --git a/src/Recommender/Service/RecommendedUsers.jsx b/src/Recommender/Service/RecommendedUsers.jsx new file mode 100644 index 00000000..bb33c21c --- /dev/null +++ b/src/Recommender/Service/RecommendedUsers.jsx @@ -0,0 +1,198 @@ +const Button = styled.button` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 36px 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const H1 = styled.h1` + font-weight: 600; + font-size: 64px; + line-height: normal; + color: #11181c; + margin: 4rem 0; +`; + +const Profile = styled.div``; + +const Profiles = styled.div` + display: grid; + grid-template-columns: ${props.gridCols + ? props.gridCols + : "repeat(4, minmax(0, 1fr))"}; + gap: 24px; + + @media (max-width: 1024px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (max-width: 800px) { + grid-template-columns: minmax(0, 1fr); + } +`; + +const RecommendedUsers = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: ${props.sidebar ? "12px" : "50px"}; +`; + +State.init({ + currentPage: 1, + isLoading: true, + error: null, + totalPages: 1, + displayedUsers: [], +}); + +const updateState = (data, totalPageNum) => { + State.update({ + isLoading: false, + displayedUsers: [...state.displayedUsers, ...data], + totalPages: totalPageNum, + }); +}; + +State.update({ + displayedUsers: props.returnElements + ? state.displayedUsers.slice(0, props.returnElements) + : state.displayedUsers, +}); + +const passedContext = props.fromContext; +const fromContext = { ...passedContext, scope: props.scope }; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; + +const accountId = props.accountId; +const profile = props.profile || Social.get(`${accountId}/profile/**`, "final"); +const tags = Object.keys(profile.tags || {}); +const profileUrl = `#/${REPL_ACCOUNT}/widget/ProfilePage?accountId=${accountId}`; + +const getRecommendedUsers = (page) => { + try { + const url = `${props.dataset}_${page}.json`; + if (state.currentPage == 1) { + const res = fetch(url); + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } + } else { + asyncFetch(url).then((res) => { + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } + }); + } + } catch (error) { + console.error("Error on fetching recommended users: ", error.message); + } +}; + +const loadMore = () => { + const nextPage = state.currentPage + 1; + if (nextPage <= state.totalPages) { + State.update({ currentPage: nextPage }); + getRecommendedUsers(nextPage); + } +}; + +if (state.isLoading) { + getRecommendedUsers(state.currentPage); +} + +if (state.error) { + console.error("Error, try again later", state.error); +} + +const handleFollowed = (accountId) => { + const updatedUsers = state.displayedUsers.filter( + (user) => (user.recommended_profile || user.similar_profile) !== accountId + ); + State.update({ + displayedUsers: updatedUsers, + }); +}; + +return ( + + {state.isLoading &&

Loading...

} + + {!state.isLoading && state.displayedUsers.length < 4 ? ( + <> + {!state.isLoading && ( +
+ Follow More Users to Unlock More Personalized Recommendations, See + Who’s + + Trending + +
+ )} + + ) : ( + state.displayedUsers.map((user, index) => ( + + + handleFollowed( + user.recommended_profile || user.similar_profile + ), + }} + /> + + )) + )} +
+ {!props.returnElements && state.currentPage < state.totalPages ? ( + + ) : null} +
+); diff --git a/src/Recommender/Views/FriendsOfFriends.jsx b/src/Recommender/Views/FriendsOfFriends.jsx new file mode 100644 index 00000000..3c4c2f19 --- /dev/null +++ b/src/Recommender/Views/FriendsOfFriends.jsx @@ -0,0 +1,174 @@ +const Button = styled.button` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const Profile = styled.div``; + +const Profiles = styled.div` + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 24px; + padding-bottom: 24px; + + @media (max-width: 1024px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + @media (max-width: 800px) { + grid-template-columns: minmax(0, 1fr); + } +`; + +const RecommendedUsers = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 100px; +`; + +State.init({ + currentPage: 1, + userData: [], + isLoading: true, + error: null, + totalPages: 1, +}); + +const updateState = (data, totalPageNum) => { + State.update({ + isLoading: false, + userData: [...state.userData, ...data], + totalPages: totalPageNum, + }); +}; + +const displayedUsers = props.returnElements + ? state.userData.slice(0, props.returnElements) + : state.userData; + +const passedContext = props.fromContext; +const fromContext = { ...passedContext, scope: props.scope || null }; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; +const algorithm = "friends_of_friends"; +const dataset = `${BASE_URL}/${algorithm}_${props.accountId}`; + +const getRecommendedUsers = (page) => { + try { + const url = `${dataset}_${page}.json`; + if (state.currentPage == 1) { + const res = fetch(url); + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } + } else { + asyncFetch(url).then((res) => { + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } + }); + } + } catch (error) { + console.error("Error on fetching recommended users: ", error.message); + } +}; + +const loadMore = () => { + const nextPage = state.currentPage + 1; + if (nextPage <= state.totalPages) { + State.update({ currentPage: nextPage }); + getRecommendedUsers(nextPage); + } +}; + +if (state.isLoading) { + getRecommendedUsers(state.currentPage); +} + +if (state.error) { + console.error("Error, try again later", state.error); +} + +return ( + + {state.isLoading &&

Loading...

} + + {!state.isLoading && displayedUsers.length < 4 ? ( + <> + {!state.isLoading && ( +
+ Follow More Users to Unlock More Personalized Recommendations, See + Who’s + + Trending + +
+ )} + + ) : ( + displayedUsers.map((user, rank) => ( + + + + )) + )} +
+ {!props.returnElements && state.currentPage < state.totalPages ? ( + + ) : null} +
+); diff --git a/src/Recommender/Views/RecommendedAvatars.jsx b/src/Recommender/Views/RecommendedAvatars.jsx new file mode 100644 index 00000000..7c1b3c97 --- /dev/null +++ b/src/Recommender/Views/RecommendedAvatars.jsx @@ -0,0 +1,59 @@ +const Avatar = styled.div` + display: flex; + width: ${props.avatarSize || "40px"}; + height: ${props.avatarSize || "40px"}; + flex-shrink: 0; + border: 1px solid #eceef0; + overflow: hidden; + border-radius: ${props.avatarSize || "40px"}; + background-color: white; + + img { + object-fit: cover; + width: 100%; + height: 100%; + } +`; + +const AvatarContainer = styled.div` + display: flex; + padding-right: 7px; + + & > div { + margin-right: -8px; + border: 2px solid white; + } + & > div:nth-child(n + 4) { + display: none; + } +`; + +const profiles = props.becauseYouFollow; +const account = Social.get(`${accountId}/profile/**`, "final"); +const fourProfiles = profiles.slice(0, 4); + +const avatarData = fourProfiles.map((profile) => { + return Social.get(`${profile}/profile/**`, "final"); +}); + +return ( + <> + {profiles ? ( + + {avatarData.map((avatar, index) => ( + + + + ))} + + ) : null} + +); diff --git a/src/Recommender/Views/RecommendedUsersSidebar.jsx b/src/Recommender/Views/RecommendedUsersSidebar.jsx new file mode 100644 index 00000000..2c4e8c5b --- /dev/null +++ b/src/Recommender/Views/RecommendedUsersSidebar.jsx @@ -0,0 +1,107 @@ +const ButtonLink = styled.a` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const CategoryHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const RecommendationsView = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const H3 = styled.h3` + font-weight: 600; + font-size: 24px; + line-height: normal; + color: #11181c; + margin: 1.8rem 0 1.5rem 0; +`; + +const RetroLinkButton = styled.button` + background: none; + border: none; + cursor: pointer; + color: blue; + font-size: 16px; + padding: 0; + margin: 0; + outline: none; + transition: color 0.3s; + + &:hover { + color: purple; + } + + &:active { + color: red; + } +`; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; +const recommendedProfiles = "friends_of_friends"; +const similarProfiles = "similarity_estimation"; + +const recommendedProfilesURL = `${BASE_URL}/${recommendedProfiles}_${context.accountId}`; +const similarProfilesURL = `${BASE_URL}/${similarProfiles}_${context.accountId}`; + +return ( + + + + + + View Recommended Profiles + + +); diff --git a/src/Recommender/Views/RecommendedUsersView.jsx b/src/Recommender/Views/RecommendedUsersView.jsx new file mode 100644 index 00000000..580417bf --- /dev/null +++ b/src/Recommender/Views/RecommendedUsersView.jsx @@ -0,0 +1,154 @@ +const ButtonLink = styled.a` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const CategoryHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const RecommendationsView = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const H3 = styled.h3` + font-weight: 600; + font-size: 24px; + line-height: normal; + color: #11181c; + margin: 1.8rem 0 1.5rem 0; +`; + +const RetroLinkButton = styled.button` + background: none; + border: none; + cursor: pointer; + color: blue; + font-size: 16px; + padding: 0; + margin: 0; + outline: none; + transition: color 0.3s; + + &:hover { + color: purple; + } + + &:active { + color: red; + } +`; + +State.init({ + expandedList: "", +}); + +const handleToggleList = (listId) => { + if (state.expandedList === listId) { + State.update({ expandedList: "" }); + } else { + State.update({ expandedList: listId }); + } +}; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; + +const similarProfiles = "similarity_estimation"; + +const recommendedProfilesURL = `${BASE_URL}/${recommendedProfiles}_${context.accountId}`; +const similarProfilesURL = `${BASE_URL}/${similarProfiles}_${context.accountId}`; + +return ( + + {state.expandedList !== "list2" && ( + <> + {props.sidebar ? ( + <> + ) : ( + +

Friends of Friends

+ handleToggleList("list1")}> + {state.expandedList === "list1" + ? "Back to categories" + : "View all"} + +
+ )} + + + )} + + {state.expandedList !== "list1" && ( + <> + {props.sidebar ? ( + <> + ) : ( + +

Similar to you

+ handleToggleList("list2")}> + {state.expandedList === "list2" + ? "Back to categories" + : "View all"} + +
+ )} + + + )} + + {props.sidebar && ( + + View Recommended Profiles + + )} +
+); diff --git a/src/Recommender/Views/SimilarProfiles.jsx b/src/Recommender/Views/SimilarProfiles.jsx new file mode 100644 index 00000000..82f66283 --- /dev/null +++ b/src/Recommender/Views/SimilarProfiles.jsx @@ -0,0 +1,182 @@ +const Button = styled.button` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const Profile = styled.div``; + +const Profiles = styled.div` + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 24px; + padding-bottom: 24px; + + @media (max-width: 1024px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + @media (max-width: 800px) { + grid-template-columns: minmax(0, 1fr); + } +`; + +const RecommendedUsers = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 100px; +`; + +State.init({ + currentPage: 1, + userData: [], + isLoading: true, + error: null, + totalPages: 1, +}); + +const updateState = (data, totalPageNum) => { + State.update({ + isLoading: false, + userData: [...state.userData, ...data], + totalPages: totalPageNum, + }); +}; + +const displayedUsers = props.returnElements + ? state.userData.slice(0, props.returnElements) + : state.userData; + +const passedContext = props.fromContext; +const fromContext = { ...passedContext, scope: props.scope || null }; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; +const algorithm = "similarity_estimation"; +const dataset = `${BASE_URL}/${algorithm}_${props.accountId}`; + +const getRecommendedUsers = (page) => { + try { + const url = `${dataset}_${page}.json`; + if (state.currentPage == 1) { + const res = fetch(url); + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } else { + console.log( + "Error fetching data. Try reloading the page, or no data available." + ); + } + } else { + asyncFetch(url).then((res) => { + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } else { + console.log( + "Error fetching data. Try reloading the page, or no data available." + ); + } + }); + } + } catch (error) { + console.error("Error on fetching recommended users: ", error.message); + } +}; + +const loadMore = () => { + const nextPage = state.currentPage + 1; + if (nextPage <= state.totalPages) { + State.update({ currentPage: nextPage }); + getRecommendedUsers(nextPage); + } +}; + +if (state.isLoading) { + getRecommendedUsers(state.currentPage); +} + +if (state.error) { + console.error("Error, try again later", state.error); +} + +return ( + + {state.isLoading &&

Loading...

} + + {!state.isLoading && displayedUsers.length < 4 ? ( + <> + {!state.isLoading && ( +
+ Follow More Users to Unlock More Personalized Recommendations, See + Who’s + + Trending + +
+ )} + + ) : ( + displayedUsers.map((user, index) => ( + + + + )) + )} +
+ {!props.returnElements && state.currentPage < state.totalPages ? ( + + ) : null} +
+); diff --git a/src/Recommender/Views/TrendingUsersSidebar.jsx b/src/Recommender/Views/TrendingUsersSidebar.jsx new file mode 100644 index 00000000..55aad43b --- /dev/null +++ b/src/Recommender/Views/TrendingUsersSidebar.jsx @@ -0,0 +1,63 @@ +const ButtonLink = styled.a` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const TrendingUsersView = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; +const algorithm = "trending_users"; +const dataset = `${BASE_URL}/${algorithm}`; + +return ( + <> + + + + {props.sidebar && ( + <> + + View Trending Users + + + )} + +); diff --git a/src/Recommender/Views/TrendingUsersView.jsx b/src/Recommender/Views/TrendingUsersView.jsx new file mode 100644 index 00000000..3803fcfc --- /dev/null +++ b/src/Recommender/Views/TrendingUsersView.jsx @@ -0,0 +1,202 @@ +const Button = styled.button` + display: block; + width: 100%; + padding: 8px; + height: 32px; + background: #fbfcfd; + border: 1px solid #d7dbdf; + border-radius: 50px; + font-weight: 600; + font-size: 12px; + line-height: 15px; + text-align: center; + cursor: pointer; + color: #11181c !important; + margin: 0; + + &:hover, + &:focus { + background: #ecedee; + text-decoration: none; + outline: none; + } + + span { + color: #687076 !important; + } +`; + +const Profile = styled.div``; + +const Profiles = styled.div` + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 24px; + padding-bottom: 24px; + + @media (max-width: 1024px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + @media (max-width: 800px) { + grid-template-columns: minmax(0, 1fr); + } +`; + +const RecommendedUsers = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 100px; +`; + +State.init({ + currentPage: 1, + userData: [], + isLoading: true, + error: null, + totalPages: 1, +}); + +const updateState = (data, totalPageNum) => { + State.update({ + isLoading: false, + userData: [...state.userData, ...data], + totalPages: totalPageNum, + }); +}; + +const displayedUsers = props.returnElements + ? state.userData.slice(0, props.returnElements) + : state.userData; + +const passedContext = props.fromContext; +const fromContext = { ...passedContext, scope: props.scope || null }; + +const STORE = "storage.googleapis.com"; +const BUCKET = "databricks-near-query-runner"; +const BASE_URL = `https://${STORE}/${BUCKET}/output/recommendations`; +const algorithm = "trending_users"; +const dataset = `${BASE_URL}/${algorithm}`; + +const getRecommendedUsers = (page) => { + try { + const url = `${dataset}_${page}.json`; + if (state.currentPage == 1) { + const res = fetch(url); + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } + } else { + asyncFetch(url).then((res) => { + if (res.ok) { + const parsedResults = JSON.parse(res.body); + const totalPageNum = parsedResults.total_pages || 10; + updateState(parsedResults.data, totalPageNum); + } + }); + } + } catch (error) { + console.log(error.message); + } +}; + +const loadMore = () => { + const nextPage = state.currentPage + 1; + if (nextPage <= state.totalPages) { + State.update({ currentPage: nextPage }); + getRecommendedUsers(nextPage); + } +}; + +if (state.isLoading) { + getRecommendedUsers(state.currentPage); +} + +if (state.error) { + console.error("Error, try again later", state.error); +} + +return ( + + {state.isLoading && displayedUsers.length == null &&

Loading...

} + {(displayedUsers.length < 4 || displayedUsers == null) && + state.isLoading && + (props.scope == "friends" || props.scope === "similar") && ( +

+ Follow More Users to Unlock More Personalized Recommendations, See + Who’s + + Trending + +

+ )} + {!props.sidebar && ( + + {state.userData.map((user, index) => ( + + + + ))} + + )} + {props.sidebar && ( + + {state.userData.map((user, index) => ( + + + + ))} + + )} + {state.currentPage < state.totalPages ? ( + + ) : null} +
+); diff --git a/src/Select.jsx b/src/Select.jsx index f0ad9789..db77d6c5 100644 --- a/src/Select.jsx +++ b/src/Select.jsx @@ -50,8 +50,8 @@ const Input = styled.div` padding: 0.5em 0.75em; gap: 0.5em; background: #ffffff; - border: 1px solid #d0d5dd; - box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); + border: ${props.border || "1px solid #d0d5dd"}; + box-shadow: ${props.border || "0px 1px 2px rgba(16, 24, 40, 0.05)"}; border-radius: 4px; color: #101828; width: 100%;