import React, { useState, useEffect, useContext, useCallback, memo, useMemo, useRef } from "react";
import theme from "../theme.jsx";
import { isMobile } from "react-device-detect";
import AppGrid from "../components/AppGrid.jsx";
import { useLocation, useNavigate } from "react-router-dom";
import {
TextField, Button, Typography, MenuItem, Select, Tabs, Tab, Zoom,
Grid,
Paper,
ButtonBase,
Tooltip,
Box,
CircularProgress,
Checkbox,
} from "@mui/material";
import { Context } from "../context/ContextApi.jsx";
import Add from '@mui/icons-material/Add';
import InputAdornment from '@mui/material/InputAdornment';
import Search from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import { ClearRefinements, connectHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom";
import { removeQuery } from "../components/ScrollToTop.jsx";
import { toast } from "react-toastify";
import algoliasearch from "algoliasearch/lite";
import { debounce } from "lodash";
import AppSelection from "../components/AppSelection.jsx";
const searchClient = algoliasearch(
"JNSS5CFDZZ",
"db08e40265e2941b9a7d8f644b6e5240"
);
// AppCard Component
const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, deactivatedIndexes, currTab }) => {
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "localhost:3000";
const appUrl = isCloud ? `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`;
const paperStyle = {
backgroundColor: mouseHoverIndex === index ? "rgba(26, 26, 26, 1)" : "#1A1A1A",
color: "rgba(241, 241, 241, 1)",
cursor: "pointer",
position: "relative",
width: 365,
height: 96,
borderRadius: 8,
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)",
marginBottom: 20,
};
return (
setMouseHoverIndex(index)}
onMouseOut={() => setMouseHoverIndex(-1)}
>
{
// if (!event.target.closest('.deactivate-button')) {
// window.location.href = appUrl;
// }
console.log("clicked");
}}
>
{data.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
{data.categories ? data.categories.join(", ") : "NA"}
{data.generated !== true && data.tags && data.tags.slice(0, 2).map((tag, tagIndex) => (
{tag}
{tagIndex < data.tags.length - 1 ? ", " : ""}
))}
{/* Deactivate button */}
{currTab === 0 && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true && (
)}
);
};
// Component to fetch all public app from the algolia.
const Hits = ({
userdata,
hits,
setIsAnyAppActivated,
searchQuery,
globalUrl,
isLoading,
isLoggedIn,
}) => {
const [hoverEffect, setHoverEffect] = useState(-1);
const [allActivatedAppIds, setAllActivatedAppIds] = useState(userdata?.active_apps);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
const normalizedString = (name) => {
if (typeof name === 'string') {
return name.replace(/_/g, ' ');
} else {
return name;
}
};
//Function for activation and deactivation of app
const handleActivateButton = (event, data, type) => {
//use prevent default so it will stop redirection to the app page
event.preventDefault();
if (!isLoggedIn) {
toast.error("Please log in to your account to activate the app.")
return;
}
if (type === "activate") {
toast.success(`The ${normalizedString(data.name)} app is activating. Please wait...`);
}
if (type === "deactivate") {
toast.success(`The ${normalizedString(data.name)} app is deactivating. Please wait...`);
}
const baseURL = globalUrl;
const url = `${baseURL}/api/v1/apps/${data.objectID}/${type}`;
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success === false) {
toast.error(responseJson.reason);
} else {
//toast.success(`App ${type}d Successfully!`);
if (type === 'activate') {
setAllActivatedAppIds(prev => [...prev, data.objectID]);
setIsAnyAppActivated(true);
}
if (type === 'deactivate') {
const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID);
setAllActivatedAppIds(updatedIds);
}
}
})
.catch(error => {
console.log("app error: ", error.toString());
});
}
let workflowDelay = 0;
const isHeader = true;
const [showNoAppFound, setShowNoAppFound] = useState(false);
//show some delay to show the "App Not Found." so it doesn't not show while changing tab.
useEffect(() => {
const timer = setTimeout(() => {
setShowNoAppFound(true);
}, 1000);
return () => clearTimeout(timer);
}, []);
return (
{!isLoading ? (
{hits?.length === 0 && searchQuery.length >= 0 && showNoAppFound ? (
No Apps Found
) : (
{hits?.map((data, index) => {
const appUrl =
isCloud
? `/apps/${data.objectID}?queryID=${data.__queryID}`
: `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`;
return (
{
setHoverEffect(index);
}}
onMouseLeave={() => {
setHoverEffect(-1);
}}
>
{(allActivatedAppIds && allActivatedAppIds.includes(data.objectID)) && }
{normalizedString(data.name)}
{data.categories !== null
? normalizedString(data.categories).join(", ")
: "NA"}
{hoverEffect === index && isCloud ? (
{data.tags && (
{data.tags.slice(0, 1).map((tag, tagIndex) => (
{normalizedString(tag)}
{tagIndex < 1 ? ", " : ""}
))}
)}
) : (
{data.tags &&
data.tags.map((tag, tagIndex) => (
{normalizedString(tag)}
{tagIndex < data.tags.length - 1 ? ", " : ""}
))}
)}
{hoverEffect === index && isCloud && (
{allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? (
) : (
)}
)}
);
})}
)}
) : (
)}
);
}
// Custom Category Dropdown Component
const CategoryDropdown = ({ items, currentRefinement, refine }) => {
const handleChange = (event) => {
const value = event.target.value;
refine(value);
};
return (
);
};
const CustomCategoryDropdown = connectRefinementList(CategoryDropdown);
// Custom SearchBox Component
const SearchBox = ({ refine, searchQuery, setSearchQuery }) => {
const inputRef = useRef(null);
const [localQuery, setLocalQuery] = useState(searchQuery);
// Debounced function to refine search
const debouncedRefine = useRef(
debounce((value) => {
setSearchQuery(value);
removeQuery("q");
refine(value);
}, 300) // Adjust the delay as needed
).current;
useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const foundQuery = params["q"];
if (foundQuery) {
setLocalQuery(foundQuery);
debouncedRefine(foundQuery);
}
}, [debouncedRefine]);
useEffect(() => {
if (searchQuery === "") {
return;
}
if (inputRef.current) {
inputRef.current.focus();
}
}, [searchQuery]);
const handleChange = (event) => {
const value = event.target.value;
setLocalQuery(value);
debouncedRefine(value);
};
return (
{
if (event.key === "Enter") {
event.preventDefault();
}
}}
style={{ width: '100%', borderRadius: '7px', fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}
InputProps={{
style: {
borderRadius: 8,
},
endAdornment: (
{localQuery?.length === 0 ? : (
{
setLocalQuery('');
debouncedRefine('');
}}
/>
)}
),
}}
/>
);
};
const CustomSearchBox = connectSearchBox(SearchBox);
const CustomHits = connectHits(Hits);
// Custom Label Dropdown Component
const LabelDropdown = ({ items, currentRefinement, refine }) => {
const handleChange = (event) => {
const value = event.target.value;
refine(value);
};
return (
);
};
const CustomLabelDropdown = connectRefinementList(LabelDropdown);
// Main Apps Component
const Apps2 = (props) => {
const { globalUrl, isLoaded, serverside, userdata, isLoggedIn, checkLogin } = props;
let navigate = useNavigate();
const { leftSideBarOpenByClick } = useContext(Context);
const location = useLocation();
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState([]);
const [selectedLabel, setSelectedLabel] = useState([]);
const [currTab, setCurrTab] = useState(0);
const [categories, setCategories] = useState([]);
const [labels, setLabels] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [userApps, setUserApps] = useState([]);
const [orgApps, setOrgApps] = useState([]);
const [appsToShow, setAppsToShow] = useState([]);
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
const [IsAnyAppActivated, setIsAnyAppActivated] = useState(false);
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
var counted = 0;
const [showNoAppFound, setShowNoAppFound] = useState(false);
const [appFramework, setAppFramework] = useState(undefined);
const [defaultSearch, setDefaultSearch] = useState("");
// Set the current tab based on the query parameter
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const tabParam = queryParams.get('tab');
if (tabParam !== null && tabParam !== undefined) {
if (tabParam === 'org_apps') {
setCurrTab(0);
} else if (tabParam === 'my_apps') {
setCurrTab(1);
} else {
setCurrTab(2);
}
}
}, [location.search]);
useEffect(() => {
const getFramework = () => {
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
setAppFramework({})
if (responseJson.reason !== undefined) {
//toast("Failed loading: " + responseJson.reason)
} else {
//toast("Failed to load framework for your org.")
}
} else {
setAppFramework(responseJson)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
})
}
getFramework();
}, []);
// Fetch apps based on the current tab : 0 -> org_apps, 1 -> my_apps, 2 -> all_apps
useEffect(() => {
const fetchApps = async () => {
const baseUrl = globalUrl;
let url;
const userId = userdata?.id;
if (currTab === 1 && userId) {
url = `${baseUrl}/api/v1/users/${userId}/apps`;
} else if (currTab === 0) {
url = `${baseUrl}/api/v1/apps`;
}
setIsLoading(true);
try {
const response = await fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (currTab === 1) {
setAppsToShow(data);
setUserApps(data);
} else if (currTab === 0) {
setAppsToShow(data);
setOrgApps(data);
// For testing the empty state
// setAppsToShow([]);
// setOrgApps([]);
}
setIsLoading(false);
} catch (err) {
console.error("Error fetching apps:", err);
setIsLoading(false);
}
};
// Only fetch if we have required data
if (globalUrl && (currTab === 0 || (currTab === 1 && userdata?.id))) {
fetchApps();
}
}, [currTab, globalUrl, userdata?.id]); // Remove location.search dependency
useEffect(() => {
setSearchQuery("");
}, [currTab])
// Find top categories and tags based on the current tab
useEffect(() => {
if (currTab === 0) {
setCategories(findTopCategories());
setLabels(findTopTags());
}
}, [currTab, appsToShow])
useEffect(() => {
if (serverside) {
return null;
}
}, [serverside]);
const findTopCategories = () => {
const categoryCountMap = {};
const apps = currTab === 1 ? userApps : orgApps;
// Check if userAndOrgsApp is an array before iterating over it and Find top 10 Category from the apps
if (Array.isArray(apps)) {
apps.forEach((app) => {
const categories = app.categories;
if (categories && categories.length > 0) {
categories.forEach((category) => {
categoryCountMap[category] = (categoryCountMap[category] || 0) + 1;
});
}
});
const categoryArray = Object.keys(categoryCountMap).map((category) => ({
category,
count: categoryCountMap[category],
}));
categoryArray.sort((a, b) => b.count - a.count);
const topCategories = categoryArray.slice(0, 7);
return topCategories;
}
};
const findTopTags = () => {
const tagCountMap = {};
const apps = currTab === 1 ? userApps : orgApps;
if (Array.isArray(apps)) {
apps.forEach((app) => {
const tags = app.tags;
if (tags && tags.length > 0) {
tags.forEach((tag) => {
tagCountMap[tag] = (tagCountMap[tag] || 0) + 1;
});
}
});
}
const tagArray = Object.keys(tagCountMap).map((tag) => ({
tag,
count: tagCountMap[tag],
}));
tagArray.sort((a, b) => b.count - a.count);
const topTags = tagArray.slice(0, 8);
return topTags;
};
const handleCreateApp = (e) => {
e.preventDefault();
console.log("Create app clicked")
};
useEffect(() => {
const apps = currTab === 1 ? userApps : orgApps;
// Search app based on app name, category, and tag
const filteredUserAppdata = Array.isArray(apps) ? apps.filter((app) => {
const matchesSearchQuery = (
searchQuery === "" || // If searchQuery is empty, match all apps
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(app.tags && app.tags.some(tag =>
tag.toLowerCase().includes(searchQuery.toLowerCase())
)) ||
(app.categories && app.categories.some((category) =>
category.toLowerCase().includes(searchQuery.toLowerCase())
))
);
const matchesSelectedCategories = (
selectedCategory.length === 0 || // If no category is selected, match all apps
(app.categories && app.categories.some(category =>
selectedCategory.includes(category)
))
);
const matchesSelectedTags = (
selectedLabel.length === 0 || // If no label is selected, match all apps
(app.tags && app.tags.some(tag =>
selectedLabel.includes(tag)
))
);
return matchesSearchQuery && matchesSelectedCategories && matchesSelectedTags;
}) : [];
setAppsToShow(filteredUserAppdata);
}, [searchQuery, selectedCategory, selectedLabel]);
const handleTabChange = (newTab) => {
setCurrTab(newTab);
if (newTab === 0) {
setAppsToShow(orgApps)
const categories = findTopCategories();
const labels = findTopTags();
setCategories(categories);
setLabels(labels);
}
if (newTab === 1) {
setAppsToShow(userApps)
}
const newQueryParam = newTab === 0 ? 'org_apps' : newTab === 1 ? 'my_apps' : 'all_apps';
const queryParams = new URLSearchParams(location.search);
queryParams.set('tab', newQueryParam);
queryParams.delete('q');
window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`);
console.log("Apps to show", appsToShow)
};
const boxStyle = {
color: "white",
display: "flex",
flexDirection: "column",
width: "100%",
margin: "auto",
maxWidth: "60%",
// padding: '20px 380px',
};
const handleCategoryChange = (event) => {
const value = event.target.value;
setSelectedCategory(value);
};
const handleLabelChange = (event) => {
const value = event.target.value;
setSelectedLabel(value);
};
return (
Apps
handleTabChange(newTab)}
TabIndicatorProps={{ style: { height: '3px', borderRadius: 10 } }}
>
{
(currTab === 0 || currTab === 1) &&
{
setSearchQuery(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
}
}}
limit={5}
style={{ width: '100%', borderRadius: '7px', fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}
InputProps={{
style: {
borderRadius: 8,
},
endAdornment: (
{
searchQuery.length === 0 ? :
}
),
}}
/>
}
{
currTab === 2 &&
}
{currTab === 2 ? (
) : (
)}
{currTab === 2 ? (
) : (
)}
{
currTab === 0 && (
{isLoading ? (
) : (
<>
{appsToShow?.length > 0 && appsToShow !== undefined && !isLoading ? (
{appsToShow.map((data, index) => (
))}
) : (
)}
>
)}
)
}
{
currTab === 1 && (
{isLoading ? (
) : (
<>
{appsToShow?.length > 0 && appsToShow !== undefined ? (
{appsToShow.map((data, index) => (
))}
) : (
No Apps Found
)}
>
)}
)
}
{
currTab === 2 &&
}
);
};
export default Apps2;