Skip to content

Commit

Permalink
feat: random projects as cloud function
Browse files Browse the repository at this point in the history
  • Loading branch information
hlolli committed Dec 15, 2024
1 parent abdbed7 commit 1607e61
Show file tree
Hide file tree
Showing 16 changed files with 244 additions and 180 deletions.
4 changes: 4 additions & 0 deletions functions/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { initializeApp } from "firebase-admin/app";
initializeApp({});

export { addProjectFileOnStorageUploadCallback as add_project_file_on_storage_upload_callback } from "./add_project_file_on_storage_upload.js";
export { deleteUserCallback as delete_user_callback } from "./delete_user.js";
export { followersCounter as followers_counter } from "./followers_counter.js";
Expand All @@ -6,3 +9,4 @@ export { host } from "./host.js";
export { newUserCallback as new_user_callback } from "./new_user.js";
export { projectFileStorageDeleteCallback as project_file_storage_delete_callback } from "./project_file_storage_delete.js";
export { projectsCounter as projects_counter } from "./projects_counter.js";
export { randomProjects as random_projects } from "./random_projects.js";
27 changes: 27 additions & 0 deletions functions/src/random_projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import admin from "firebase-admin";
import { onCall } from "firebase-functions/v2/https";
import { log } from "firebase-functions/logger";
import { shuffle } from "lodash";

let lastUpdate = Date.now();
let projects: any[] = [];

export const randomProjects = onCall<{ count: number }>(async ({ data }) => {
if (projects.length === 0 || Date.now() - lastUpdate > 1000 * 60 * 5) {
lastUpdate = Date.now();
const db = admin.firestore();
const projectsCollection = await db
.collection("projects")
.where("public", "==", true)
.orderBy("createdAt", "desc")
.limit(100)
.get();
projects = projectsCollection.docs.map((doc) => ({
...doc.data(),
projectUid: doc.id
}));
log("projectsLength: " + projects.length);
}

return shuffle(projects).slice(0, data.count);
});
10 changes: 9 additions & 1 deletion src/components/header/project-profile-meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ const ProjectProfileMeta = (): React.ReactElement => {
<div>
<div css={SS.projectProfileTooltipTitleContainer}>
<div css={SS.projectIcon}>
<ProjectAvatar project={project} />
<ProjectAvatar
iconName={project.iconName}
iconBackgroundColor={
project.iconBackgroundColor
}
iconForegroundColor={
project.iconForegroundColor
}
/>
</div>
<div style={{ marginLeft: 24 }}>
<h1 css={SS.projectProfileMetaH1}>
Expand Down
100 changes: 44 additions & 56 deletions src/components/home/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { difference, keys, isEmpty, pluck } from "ramda";
import { getFunctions, httpsCallable } from "firebase/functions";
import { documentId, getDocs, query, where } from "firebase/firestore";
import { profiles, projects } from "@config/firestore";
import { RootState } from "@root/store";
Expand All @@ -10,7 +11,8 @@ import {
SEARCH_PROJECTS_SUCCESS,
SET_POPULAR_PROJECTS_OFFSET,
SET_RANDOM_PROJECTS_LOADING,
HomeActionTypes
HomeActionTypes,
RandomProjectResponse
} from "./types";
import { IProject } from "@comp/projects/types";
import {
Expand All @@ -21,6 +23,12 @@ import { IStarredProjectSearchResult, IStarredProject } from "@db/search";

const databaseID = process.env.REACT_APP_DATABASE === "DEV" ? "dev" : "prod";
const searchURL = `https://web-ide-search-api.csound.com/search/${databaseID}`;
const functions = getFunctions();
const getRandomProjects = httpsCallable<
{ count: number },
RandomProjectResponse[]
>(functions, "random_projects");

// const searchURL = `http://localhost:4000/search/${databaseID}`;

export const searchProjects =
Expand Down Expand Up @@ -96,17 +104,19 @@ export const fetchPopularProjects = (offset = 0, pageSize = 8) => {
let starsIDs: string[] = [];
let totalRecords = 0;
try {
const starsRequest = await fetch(
`${searchURL}/list/stars/${pageSize}/${offset}/count/desc`
);
const starredProjects: IStarredProjectSearchResult =
await starsRequest.json();
// const starsRequest = await fetch(
// `${searchURL}/list/stars/${pageSize}/${offset}/count/desc`
// );
// const starredProjects: IStarredProjectSearchResult =
// await starsRequest.json();

const starredProjects = { data: [] };

starsIDs = starredProjects.data.map(
(item: IStarredProject) => item.id
);

totalRecords = starredProjects.totalRecords as number;
totalRecords = 0; //starredProjects.totalRecords as number;
} catch (error) {
console.error(error);
}
Expand Down Expand Up @@ -168,68 +178,46 @@ export const fetchRandomProjects = () => {

const state = getState().HomeReducer;

let randomProjectsIDs: string[] = [];
let randomProjects: RandomProjectResponse[] = [];

try {
const randomProjectsRequest = await fetch(
`${searchURL}/random/projects/8`
);

const randomProjects = await randomProjectsRequest.json();

randomProjectsIDs = randomProjects.data.map(
(item: IStarredProject) => item.id
);
const randomProjectsResponse = await getRandomProjects({
count: 8
});
randomProjects = randomProjectsResponse.data;
} catch (error) {
console.error(error);
}

if (!isEmpty(randomProjectsIDs)) {
const randomProjectsSnapshots = await getDocs(
query(
query(projects, where("public", "==", true)),
where(documentId(), "in", randomProjectsIDs)
)
);

const randomProjects: IProject[] = await Promise.all(
randomProjectsSnapshots.docs.map(
async (snap) => await convertProjectSnapToProject(snap)
)
);

const userIDs = pluck("userUid", randomProjects);

const missingProfiles = difference(userIDs, keys(state.profiles));

if (!isEmpty(missingProfiles)) {
const projectProfiles = {};
const userIDs = randomProjects.map((project) => project.userUid);
const missingProfiles = difference(userIDs, keys(state.profiles));

const profilesQuery = await getDocs(
query(profiles, where(documentId(), "in", missingProfiles))
);
if (!isEmpty(missingProfiles)) {
const projectProfiles = {};

profilesQuery.forEach((snapshot) => {
projectProfiles[snapshot.id] = snapshot.data();
if (projectProfiles[snapshot.id]?.userJoinDate) {
projectProfiles[snapshot.id].userJoinDate =
projectProfiles[
snapshot.id
].userJoinDate.toMillis();
}
});
const profilesQuery = await getDocs(
query(profiles, where(documentId(), "in", missingProfiles))
);

dispatch({
type: ADD_USER_PROFILES,
payload: projectProfiles
});
}
profilesQuery.forEach((snapshot) => {
projectProfiles[snapshot.id] = snapshot.data();
if (projectProfiles[snapshot.id]?.userJoinDate) {
projectProfiles[snapshot.id].userJoinDate =
projectProfiles[snapshot.id].userJoinDate.toMillis();
}
});

dispatch({
type: ADD_RANDOM_PROJECTS,
payload: randomProjects
type: ADD_USER_PROFILES,
payload: projectProfiles
});
}

dispatch({
type: ADD_RANDOM_PROJECTS,
payload: randomProjects
});

dispatch({ type: SET_RANDOM_PROJECTS_LOADING, isLoading: false });
};
};
5 changes: 3 additions & 2 deletions src/components/home/home.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "@root/store";
import Header from "@comp/header/header";
import Search from "./search";
// import Search from "./search";
import PopularProjects from "./popular-projects";
import RandomProjects from "./random-projects";
import { homeBackground } from "./background-style";
Expand Down Expand Up @@ -79,7 +79,8 @@ const Home = (): React.ReactElement => {
<>
<Header />
<div css={homeBackground}>
<Search />
{/* <Search /> */}
<p>Search is being fixed...</p>
<PopularProjects
projects={currentPopularProjectsPagination || []}
handlePopularProjectsNextPage={
Expand Down
2 changes: 1 addition & 1 deletion src/components/home/popular-projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import LeftIcon from "@mui/icons-material/ArrowBack";
import RightIcon from "@mui/icons-material/ArrowForward";
import IconButton from "@mui/material/IconButton";
import { Theme, useTheme } from "@emotion/react";
import ProjectCard, { ProjectCardSkeleton } from "./project-card";
import { ProjectCard, ProjectCardSkeleton } from "./project-card";
import * as SS from "./styles";

const PopularProjects = ({
Expand Down
30 changes: 17 additions & 13 deletions src/components/home/project-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import { Bars as BarsSpinner } from "react-loader-spinner";
import { Theme } from "@emotion/react";
import ProjectAvatar from "@elem/project-avatar";
import ListPlayButton from "@comp/profile/list-play-button";
import { ListPlayButton } from "@comp/profile/list-play-button";
import { IProject } from "@comp/projects/types";
import { IProfile } from "@comp/profile/types";
import {
Expand All @@ -19,13 +19,10 @@ import {
Photo,
ProjectCardContentBottomID
} from "./home-ui";
import { RandomProjectResponse } from "./types";
import * as SS from "./styles";

export const ProjectCardSkeleton = ({
theme
}: {
theme: Theme;
}): React.ReactElement => (
export const ProjectCardSkeleton = ({ theme }: { theme: Theme }) => (
<div css={SS.cardLoderSkeleton}>
<span className="skeleton-photo" />
<span className="skeleton-name" />
Expand All @@ -34,19 +31,23 @@ export const ProjectCardSkeleton = ({
</div>
);

const ProjectCard = ({
export const ProjectCard = ({
projectIndex,
profile,
project
}: {
projectIndex: number;
profile: IProfile;
project: IProject;
}): React.ReactElement => {
project: IProject | RandomProjectResponse;
}) => {
return (
<ProjectCardContainer duration={200} projectIndex={projectIndex}>
<div css={SS.cardBackground}>
<ProjectAvatar project={project} />
<ProjectAvatar
iconName={project.iconName}
iconBackgroundColor={project.iconBackgroundColor}
iconForegroundColor={project.iconForegroundColor}
/>
</div>
<ProjectCardContentContainer duration={200}>
<ProjectCardContentTop to={`editor/${project.projectUid}`}>
Expand All @@ -58,7 +59,12 @@ const ProjectCard = ({
</ProjectCardContentTopDescription>
</ProjectCardContentTop>
<ProjectCardContentMiddle>
<ListPlayButton project={project} />
<ListPlayButton
projectUid={project.projectUid}
iconName={project.iconName}
iconBackgroundColor={project.iconBackgroundColor}
iconForegroundColor={project.iconForegroundColor}
/>
</ProjectCardContentMiddle>
<ProjectCardContentBottom to={`profile/${profile.username}`}>
<ProjectCardContentBottomPhoto>
Expand All @@ -77,5 +83,3 @@ const ProjectCard = ({
</ProjectCardContainer>
);
};

export default ProjectCard;
44 changes: 21 additions & 23 deletions src/components/home/random-projects.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
import React from "react";
import { useEffect, useState } from "react";
import { RootState, useDispatch, useSelector } from "@root/store";
import IconButton from "@mui/material/IconButton";
import ShuffleIcon from "@mui/icons-material/Shuffle";
import { path, range } from "ramda";
import { range } from "ramda";
import { fetchRandomProjects } from "./actions";
import { useTheme } from "@emotion/react";
import ProjectCard, { ProjectCardSkeleton } from "./project-card";
import { RandomProjectResponse } from "./types";
import { ProjectCard, ProjectCardSkeleton } from "./project-card";
import * as SS from "./styles";

const RandomProjects = (): React.ReactElement => {
const RandomProjects = () => {
const dispatch = useDispatch();
const [isMounted, setIsMounted] = React.useState(false);
const [isMounted, setIsMounted] = useState(false);

React.useEffect(() => {
useEffect(() => {
if (!isMounted) {
setIsMounted(true);
let randomProjects;
try {
randomProjects = fetchRandomProjects();
} catch (error) {
console.error(error);
}
randomProjects && dispatch(randomProjects);
dispatch(fetchRandomProjects());
}
}, [dispatch, isMounted, setIsMounted]);
const theme = useTheme();

const theme = useTheme();
const profiles = useSelector((store: RootState) => {
return path(["HomeReducer", "profiles"], store);
return store.HomeReducer.profiles;
});

const randomProjects = useSelector((store: RootState) => {
return path(["HomeReducer", "randomProjects"], store);
});
const randomProjects: RandomProjectResponse[] = useSelector(
(store: RootState) => {
return store.HomeReducer.randomProjects;
}
);

const randomProjectsLoading = useSelector((store: RootState) => {
return path(["HomeReducer", "randomProjectsLoading"], store);
const randomProjectsLoading: boolean = useSelector((store: RootState) => {
return store.HomeReducer.randomProjectsLoading;
});

return (
Expand All @@ -60,12 +57,13 @@ const RandomProjects = (): React.ReactElement => {
<ProjectCardSkeleton theme={theme} key={index} />
))
: randomProjects.map((project, index) => {
return profiles[project.userUid] ? (
const profile = profiles[project.userUid];
return profile ? (
<ProjectCard
key={`p${index}`}
key={`p${index}${project.projectUid}`}
projectIndex={index}
project={project}
profile={profiles[project.userUid]}
profile={profile}
/>
) : (
<ProjectCardSkeleton theme={theme} key={index} />
Expand Down
Loading

0 comments on commit 1607e61

Please sign in to comment.