= [
+ {
+ value: "single-csd",
+ label: "Single CSD",
+ description: "One commented Csound 7 project.csd file."
+ },
+ {
+ value: "split-csd",
+ label: "CSD + ORC + SCO",
+ description: "Split the new starter into project.csd, .orc, and .sco."
+ },
+ {
+ value: "empty",
+ label: "Empty",
+ description: "Start with a blank project.csd file."
+ }
+];
+
+const getStarterFiles = (
+ starterTemplate: ProjectStarterTemplate,
+ loggedInUserUid: string
+) => {
+ const withUser = (document: {
+ name: string;
+ value: string;
+ type: string;
+ }) => ({
+ ...document,
+ userUid: loggedInUserUid
+ });
+
+ switch (starterTemplate) {
+ case "split-csd":
+ return [
+ withUser(defaultSplitCsd),
+ withUser(defaultOrc),
+ withUser(defaultSco)
+ ];
+ case "empty":
+ return [withUser(emptyCsd)];
+ case "single-csd":
+ default:
+ return [withUser(defaultCsd)];
+ }
+};
const addUserProjectAction = (): ProfileActionTypes => {
return {
@@ -134,7 +194,8 @@ export const addUserProject =
projectUid: string,
iconName: string,
iconForegroundColor: string,
- iconBackgroundColor: string
+ iconBackgroundColor: string,
+ starterTemplate: ProjectStarterTemplate
) =>
async (dispatch: AppThunkDispatch, getState: () => RootState) => {
const currentState = getState();
@@ -156,31 +217,36 @@ export const addUserProject =
const newProjectReference = doc(projects);
batch.set(newProjectReference, newProject);
const filesReference = collection(newProjectReference, "files");
- const csdFileReference = doc(filesReference);
- batch.set(csdFileReference, {
- ...defaultCsd,
- userUid: loggedInUserUid
- });
- batch.set(doc(filesReference), {
- ...defaultOrc,
- userUid: loggedInUserUid
- });
- batch.set(doc(filesReference), {
- ...defaultSco,
- userUid: loggedInUserUid
+ const starterFiles = getStarterFiles(
+ starterTemplate,
+ loggedInUserUid
+ );
+ let csdFileReferenceId: string | undefined;
+
+ starterFiles.forEach((starterFile) => {
+ const fileReference = doc(filesReference);
+
+ if (starterFile.name === "project.csd") {
+ csdFileReferenceId = fileReference.id;
+ }
+
+ batch.set(fileReference, starterFile);
});
+
batch.set(
doc(targets, newProjectReference.id),
{
- targets: {
- "project.csd": {
- csoundOptions: {},
- targetName: "project.csd",
- targetType: "main",
- targetDocumentUid: csdFileReference.id
- }
- },
- defaultTarget: "project.csd"
+ targets: csdFileReferenceId
+ ? {
+ "project.csd": {
+ csoundOptions: {},
+ targetName: "project.csd",
+ targetType: "main",
+ targetDocumentUid: csdFileReferenceId
+ }
+ }
+ : {},
+ defaultTarget: csdFileReferenceId ? "project.csd" : ""
},
{ merge: true }
);
@@ -191,7 +257,10 @@ export const addUserProject =
currentTags
);
dispatch(addUserProjectAction());
+ dispatch(closeModal());
+ navigateTo(`/editor/${newProjectReference.id}`);
dispatch(openSnackbar("Project Added", SnackbarType.Success));
+ return newProjectReference.id;
} catch (error) {
console.log(error);
dispatch(
@@ -306,19 +375,34 @@ export const setTagsInput = (tags: Array
): ProfileActionTypes => {
// }
// };
-export const addProject = () => {
+const openNewProjectModal = () => {
return openSimpleModal("new-project-prompt", {
name: "New Project",
description: "",
label: "Create Project",
newProject: true,
projectID: "",
+ starterTemplate: "single-csd",
iconName: undefined,
iconForegroundColor: undefined,
iconBackgroundColor: undefined
});
};
+export const addProject = () => {
+ return async (dispatch: AppThunkDispatch, getState: () => RootState) => {
+ const loggedInUserUid = selectLoggedInUid(getState());
+
+ if (loggedInUserUid) {
+ dispatch(openNewProjectModal());
+ return;
+ }
+
+ dispatch(setPostAuthFlow("create-project"));
+ dispatch(openLoginDialog("create"));
+ };
+};
+
export const followUser =
(loggedInUserUid: string, profileUid: string) => async () => {
const batch = writeBatch(database);
@@ -444,6 +528,7 @@ export const editProject = (project: IProject) => {
description: project.description,
label: "Apply changes",
projectID: project.projectUid,
+ starterTemplate: "single-csd",
iconName: project.iconName,
iconForegroundColor: project.iconForegroundColor,
iconBackgroundColor: project.iconBackgroundColor,
diff --git a/src/components/profile/profile-lists.tsx b/src/components/profile/profile-lists.tsx
index 64c6080c..b9614614 100644
--- a/src/components/profile/profile-lists.tsx
+++ b/src/components/profile/profile-lists.tsx
@@ -1,6 +1,8 @@
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
import { Link } from "react-router";
import { useDispatch, useSelector } from "@root/store";
+import { shallowEqual } from "react-redux";
+import { createSelector } from "@reduxjs/toolkit";
import { List, ListItem, ListItemText } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import LockIcon from "@mui/icons-material/Lock";
@@ -31,11 +33,14 @@ import {
StyledListButtonsContainer
} from "./profile-ui";
import { IProject } from "@comp/projects/types";
+import { IProfile } from "./types";
import { editProject, deleteProject } from "./actions";
import { markProjectPublic } from "@comp/projects/actions";
import { descend, sort, propOr } from "ramda";
import * as SS from "./styles";
+const EMPTY_STRING_ARRAY: string[] = [];
+
const ProjectListItem = ({
isProfileOwner,
project
@@ -271,19 +276,37 @@ export const ProfileLists = ({
profileUid: string;
selectedSection: number;
isProfileOwner: boolean;
- filteredProjects: Array;
+ filteredProjects: IProject[];
}) => {
- const userFollowing = useSelector(
- (state) =>
- state.ProfileReducer.profiles[profileUid]?.userFollowing ?? []
+ const userFollowingSelector = useMemo(
+ () =>
+ createSelector(
+ [(state) => state.ProfileReducer.profiles],
+ (profiles): string[] =>
+ profiles[profileUid]?.following ?? EMPTY_STRING_ARRAY
+ ),
+ [profileUid]
);
- const userFollowers = useSelector((state) =>
- (state.ProfileReducer.profiles[profileUid]?.followers ?? []).map(
- (followerUid) => state.ProfileReducer.profiles[followerUid]
- )
+ const userFollowersSelector = useMemo(
+ () =>
+ createSelector(
+ [(state) => state.ProfileReducer.profiles],
+ (profiles): Array => {
+ const followerUids =
+ profiles[profileUid]?.followers ?? EMPTY_STRING_ARRAY;
+
+ return followerUids.map(
+ (followerUid: string) => profiles[followerUid]
+ );
+ }
+ ),
+ [profileUid]
);
+ const userFollowing = useSelector(userFollowingSelector, shallowEqual);
+ const userFollowers = useSelector(userFollowersSelector, shallowEqual);
+
// Loading states
const followingLoading = useSelector(selectFollowingLoading(profileUid));
const followersLoading = useSelector(selectFollowersLoading(profileUid));
diff --git a/src/components/profile/profile-modal.tsx b/src/components/profile/profile-modal.tsx
index 0840abcb..4f84bd54 100644
--- a/src/components/profile/profile-modal.tsx
+++ b/src/components/profile/profile-modal.tsx
@@ -13,8 +13,13 @@ import Select from "react-select";
const ModalContainer = styled.div`
display: grid;
grid-auto-rows: minmax(60px, auto);
- grid-template-columns: 400px;
+ grid-template-columns: minmax(0, 400px);
+ width: min(400px, calc(100vw - 32px));
border-radius: 5px;
+
+ @media (max-width: 760px) {
+ width: min(400px, calc(100vw - 40px));
+ }
`;
interface IFieldRow {
diff --git a/src/components/profile/profile-ui.tsx b/src/components/profile/profile-ui.tsx
index 7a7711d8..462d2713 100644
--- a/src/components/profile/profile-ui.tsx
+++ b/src/components/profile/profile-ui.tsx
@@ -1,42 +1,59 @@
import Card from "@mui/material/Card";
import Chip from "@mui/material/Chip";
-import { isMobile } from "@root/utils";
import styled from "@emotion/styled";
import { css } from "@emotion/react";
+const MOBILE_BP = "(max-width: 760px)";
+const DESKTOP_BP = "(min-width: 761px)";
+
export const ProfileContainer = styled.div`
- ${isMobile()
- ? `padding: 16px;
- box-sizing: border-box;`
- : `display: grid;
- grid-template-columns: 24px 250px 800px;
- grid-template-rows: 50px 175px 1fr 70px;
- grid-auto-rows: minmax(90px, auto);`}
width: 100%;
overflow-x: hidden;
+ box-sizing: border-box;
+ @media ${MOBILE_BP} {
+ padding: 16px;
+ }
+ @media ${DESKTOP_BP} {
+ display: grid;
+ grid-template-columns: 250px 1fr;
+ grid-template-rows: 120px auto;
+ grid-auto-rows: auto;
+ align-items: start;
+ padding: 24px;
+ }
`;
export const IDContainer = styled(Card)`
- grid-row-start: 2;
- grid-row-end: 3;
- grid-column-start: 2;
- grid-column-end: 3;
+ grid-row: 1 / -1;
+ grid-column: 1 / 2;
display: grid;
- grid-template-rows: 250px 1fr auto 60px;
+ grid-template-rows: 250px 1fr auto auto;
grid-template-columns: 1fr;
z-index: 2;
min-height: 420px;
- box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8);
+ box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.6);
+ border: 1px solid rgba(255, 255, 255, 0.08);
& > div {
max-width: 250px;
}
- overflow: initial;
- height: fit-content;
+ overflow: hidden;
+ align-self: stretch;
+ position: sticky;
+ top: 70px;
+ max-height: calc(100vh - 80px);
+`;
+
+export const MobileAboutSection = styled.div`
+ padding: 24px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
`;
export const DescriptionSection = styled.div`
grid-row: ${(properties: { gridRow: string }) => properties.gridRow};
grid-column: 1;
- padding: 20px;
+ padding: 16px;
+ overflow-y: auto;
div,
a,
h1,
@@ -47,17 +64,21 @@ export const DescriptionSection = styled.div`
a > div {
font-weight: 300;
font-size: 14px;
- line-height: 32px;
+ line-height: 1.5;
white-space: nowrap;
text-decoration: underline;
}
- height: fit-content;
`;
export const EditProfileButtonSection = styled.div`
grid-row: 4;
grid-column: 1;
- margin: auto;
+ display: flex;
+ flex-direction: column;
+ padding: 12px 16px 16px;
+ gap: 8px;
+ width: 100%;
+ box-sizing: border-box;
`;
export const ProfilePictureContainer = styled.div`
@@ -114,31 +135,42 @@ export const ProfilePicture = styled.img`
object-fit: cover;
`;
export const NameSectionWrapper = styled.div`
- grid-row: 2;
- grid-column: 3;
+ grid-row: 1;
+ grid-column: 2;
+ align-self: end;
display: grid;
grid-template-rows: 1fr auto;
grid-template-columns: 1fr;
- min-width: ${isMobile() ? "0" : "680px"};
+ @media ${DESKTOP_BP} {
+ padding-left: 32px;
+ }
`;
export const NameSection = styled.div`
grid-row: 2;
grid-column: 1;
color: white;
- padding: 20px;
+ @media ${MOBILE_BP} {
+ padding: 12px 0 4px;
+ }
+ @media ${DESKTOP_BP} {
+ padding: 16px 24px;
+ }
`;
export const ContentSection = styled.div`
- grid-row-start: 3;
- grid-row-end: auto;
- grid-column: 3;
+ grid-row: 2;
+ grid-column: 2;
+ align-self: start;
z-index: 2;
- ${!isMobile() && "margin-left: 48px;"}
+ margin-top: 16px;
background: ${(properties) => properties.theme.background};
border-radius: 4px;
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8);
- width: 100%;
- max-width: 100%;
+ min-width: 0;
overflow-x: hidden;
+ min-height: 300px;
+ @media ${DESKTOP_BP} {
+ margin-left: 32px;
+ }
`;
export const ContentTabsContainer = styled.div`
background-color: rgba(0, 0, 0, 0.2);
@@ -148,15 +180,16 @@ export const contentActionsStyle = css`
display: flex;
justify-content: space-between;
align-items: center;
- height: 58px;
+ height: 56px;
padding-left: 24px;
- padding-right: 12px;
+ padding-right: 24px;
margin-top: 12px;
margin-bottom: 24px;
`;
export const ListContainer = styled.div`
- padding-top: 10px;
+ padding-top: 8px;
+ padding-bottom: 16px;
grid-row: 3;
grid-column: 1;
width: 100%;
@@ -217,8 +250,8 @@ export const StyledListItemTopRowText = styled.div`
text-align: left;
& p {
white-space: pre-line;
- padding-right: 6px;
- padding-top: 6px;
+ padding-right: 8px;
+ padding-top: 8px;
}
`;
export const StyledListItemChipsRow = styled.div`
@@ -251,7 +284,7 @@ export const StyledListButtonsContainer = styled.div`
& button {
width: 100%;
align-self: center;
- margin-top: 6px;
+ margin-top: 8px;
}
`;
diff --git a/src/components/profile/profile.tsx b/src/components/profile/profile.tsx
index cd03c3d6..e50f7dc5 100644
--- a/src/components/profile/profile.tsx
+++ b/src/components/profile/profile.tsx
@@ -1,12 +1,13 @@
import { doc, getDoc } from "firebase/firestore";
import { useDispatch, useSelector } from "@root/store";
import { ProfileLists } from "./profile-lists";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { useTheme } from "@emotion/react";
-import { isMobile, updateBodyScroller } from "@root/utils";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import { updateBodyScroller } from "@root/utils";
import { gradient } from "./gradient";
import { usernames } from "@config/firestore";
-import { useLocation, useParams, useNavigate } from "react-router";
+import { useParams, useNavigate } from "react-router";
import { createButtonAddIcon } from "./styles";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
@@ -18,8 +19,10 @@ import CameraIcon from "@mui/icons-material/CameraAltOutlined";
import AddIcon from "@mui/icons-material/Add";
import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined";
import PersonAddAlt1OutlinedIcon from "@mui/icons-material/PersonAddAlt1Outlined";
+import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
import PeopleOutlineIcon from "@mui/icons-material/PeopleOutline";
import StarBorderOutlinedIcon from "@mui/icons-material/StarBorderOutlined";
+import EditIcon from "@mui/icons-material/Edit";
import Box from "@mui/material/Box";
import SearchIcon from "@mui/icons-material/Search";
import TextField from "@mui/material/TextField";
@@ -74,9 +77,37 @@ import {
fabButton,
mobileNavigationContainer,
mobileNavigationButton,
- profileMobileBottomSpacer
+ profileMobileBottomSpacer,
+ MobileAboutSection
} from "./profile-ui";
+type ProfileSection =
+ | "projects"
+ | "following"
+ | "followers"
+ | "stars"
+ | "about";
+
+const PROFILE_ROUTE_SECTION_MAP: Record<
+ string,
+ Exclude
+> = {
+ following: "following",
+ followers: "followers",
+ stars: "stars"
+};
+
+const PROFILE_SECTION_VALUE: Record = {
+ projects: 0,
+ following: 1,
+ followers: 2,
+ stars: 3,
+ about: 4
+};
+
+const getRouteSection = (tab?: string): Exclude =>
+ PROFILE_ROUTE_SECTION_MAP[tab || ""] || "projects";
+
const UserLink = ({ link }: { link: string | undefined }) => {
return typeof link === "string" ? (
@@ -90,23 +121,43 @@ const UserLink = ({ link }: { link: string | undefined }) => {
};
export const Profile = () => {
- const [profileUid, setProfileUid]: [string | undefined, any] = useState();
+ const [profileUid, setProfileUid] = useState();
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const { username, tab } = useParams();
- const profile = useSelector(selectUserProfile(profileUid));
- const imageUrl = useSelector(selectUserImageURL(profileUid));
const loggedInUserUid = useSelector(selectLoggedInUid);
- const filteredProjects = useSelector(selectUserProjects(profileUid));
+ const profileSelector = useMemo(
+ () => selectUserProfile(profileUid),
+ [profileUid]
+ );
+ const imageUrlSelector = useMemo(
+ () => selectUserImageURL(profileUid),
+ [profileUid]
+ );
+ const projectsSelector = useMemo(
+ () => selectUserProjects(profileUid),
+ [profileUid]
+ );
+ const followingSelector = useMemo(
+ () => selectUserFollowing(loggedInUserUid),
+ [loggedInUserUid]
+ );
+
+ const profile = useSelector(profileSelector);
+ const imageUrl = useSelector(imageUrlSelector);
+ const filteredProjects = useSelector(projectsSelector);
// const followingFilterString = useSelector(selectFollowingFilterString);
const projectFilterString = useSelector(selectProjectFilterString);
const [imageHover, setImageHover] = useState(false);
- const [selectedSection, setSelectedSection] = useState(0);
- const loggedInUserFollowing: string[] = useSelector(
- selectUserFollowing(loggedInUserUid)
- );
+ const [isAboutSelected, setIsAboutSelected] = useState(false);
+ const loggedInUserFollowing: string[] = useSelector(followingSelector);
+ const isMobileLayout = useMediaQuery("(max-width: 760px)");
+ const routeSection = getRouteSection(tab);
+
+ const selectedSection: ProfileSection =
+ isMobileLayout && isAboutSelected ? "about" : routeSection;
const isFollowing = profileUid
? loggedInUserFollowing.includes(profileUid)
@@ -123,18 +174,10 @@ export const Profile = () => {
}, []);
useEffect(() => {
- if (username) {
- if (!tab && selectedSection !== 0) {
- setSelectedSection(0);
- } else if (tab === "following" && selectedSection !== 1) {
- setSelectedSection(1);
- } else if (tab === "followers" && selectedSection !== 2) {
- setSelectedSection(2);
- } else if (tab === "stars" && selectedSection !== 3) {
- setSelectedSection(3);
- }
+ if (!isMobileLayout || routeSection !== getRouteSection(tab)) {
+ setIsAboutSelected(false);
}
- }, [tab, selectedSection, username]);
+ }, [isMobileLayout, routeSection, tab]);
useEffect(() => {
if (!isRequestingLogin) {
@@ -172,7 +215,7 @@ export const Profile = () => {
),
subscribeToProfileStars(profileUid, dispatch),
subscribeToProjectsCount(profileUid, dispatch)
- ] as any[];
+ ];
// make sure the logged in user's following is listed
// when viewing another profile, for un/follow state
if (loggedInUserUid && !isProfileOwner) {
@@ -221,12 +264,153 @@ export const Profile = () => {
}
}, [displayName]);
+ const selectedSectionValue = PROFILE_SECTION_VALUE[selectedSection];
+ const isProjectsSection = selectedSection === "projects";
+ const isAboutSection = selectedSection === "about";
+
+ const handleSectionChange = (section: ProfileSection) => {
+ if (section === "about") {
+ setIsAboutSelected(true);
+ return;
+ }
+ setIsAboutSelected(false);
+ switch (section) {
+ case "projects": {
+ navigate(`/profile/${username}`);
+ return;
+ }
+ case "following": {
+ navigate(`/profile/${username}/following`);
+ return;
+ }
+ case "followers": {
+ navigate(`/profile/${username}/followers`);
+ return;
+ }
+ case "stars": {
+ navigate(`/profile/${username}/stars`);
+ return;
+ }
+ }
+ };
+
+ const profileActions = (
+ <>
+ {isProfileOwner && profileUid && (
+
+ }
+ aria-label="Edit profile settings"
+ onClick={() =>
+ profile &&
+ dispatch(
+ editProfile(
+ profile.username,
+ displayName || "",
+ bio || "",
+ link1 || "",
+ link2 || "",
+ link3 || "",
+ backgroundIndex
+ )
+ )
+ }
+ >
+ Edit Profile
+
+
+ )}
+ {!isProfileOwner && profileUid && loggedInUserUid && (
+
+
+
+ )}
+ >
+ );
+
+ const desktopProfileDetails = (
+ <>
+
+
+ Bio
+
+
+ {profile && profile.bio}
+
+
+
+
+ Links
+
+ {profile && (
+ <>
+
+
+
+ >
+ )}
+
+ {profileActions}
+ >
+ );
+
+ const mobileAboutContent = (
+
+
+
+ Bio
+
+
+ {profile?.bio || "No bio available."}
+
+
+
+
+ Links
+
+ {profile ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+ {profileActions}
+
+ );
+
return (
- {!isMobile() && (
+ {!isMobileLayout && (
setImageHover(true)}
@@ -280,93 +464,15 @@ export const Profile = () => {
)}
-
-
- Bio
-
-
- {profile && profile.bio}
-
-
-
-
- Links
-
- {profile && (
- <>
-
-
-
- >
- )}
-
- {isProfileOwner && profileUid && (
-
-
-
- )}
- {!isProfileOwner &&
- profileUid &&
- loggedInUserUid && (
-
-
-
- )}
+ {desktopProfileDetails}
)}
-
+
{profile && displayName}
@@ -374,40 +480,23 @@ export const Profile = () => {
- {!isMobile() && (
+ {!isMobileLayout && (
{
- switch (index) {
- case 0: {
- navigate(
- `/profile/${username}`
- );
- break;
- }
- case 1: {
- navigate(
- `/profile/${username}/following`
- );
- break;
- }
- case 2: {
- navigate(
- `/profile/${username}/followers`
- );
- break;
- }
- case 3: {
- navigate(
- `/profile/${username}/stars`
- );
- break;
- }
- }
- }}
+ value={selectedSectionValue}
+ onChange={(_, value: number) =>
+ handleSectionChange(
+ value === 1
+ ? "following"
+ : value === 2
+ ? "followers"
+ : value === 3
+ ? "stars"
+ : "projects"
+ )
+ }
indicatorColor={"primary"}
>
@@ -418,84 +507,96 @@ export const Profile = () => {
)}
-
- {selectedSection === 0 && (
-
-
-
- )
- }}
- onChange={(event) => {
- dispatch(
- setProjectFilterString(
- event.target.value
+ {!isAboutSection && (
+
+ {isProjectsSection && (
+
+
+
)
- );
- }}
- />
- )}
+ }}
+ onChange={(event) => {
+ dispatch(
+ setProjectFilterString(
+ event.target.value
+ )
+ );
+ }}
+ />
+ )}
- {isProfileOwner && selectedSection === 0 && (
-
- )}
-
- {profileUid && username && (
+ {isProfileOwner && isProjectsSection && (
+
+ )}
+
+ )}
+ {profileUid && username && !isAboutSection && (
)}
+ {isMobileLayout && isAboutSection && mobileAboutContent}
- {isMobile() && }
+ {isMobileLayout && }
- {isMobile() && (
+ {isMobileLayout && (
+ handleSectionChange(
+ value === 1
+ ? "following"
+ : value === 2
+ ? "followers"
+ : value === 3
+ ? "stars"
+ : value === 4
+ ? "about"
+ : "projects"
+ )
+ }
css={mobileNavigationContainer}
showLabels
>
navigate(`/profile/${username}`)}
css={mobileNavigationButton}
label="Projects"
icon={}
/>
- navigate(`/profile/${username}/following`)
- }
css={mobileNavigationButton}
label="Following"
icon={
@@ -504,22 +605,22 @@ export const Profile = () => {
/>
- navigate(`/profile/${username}/followers`)
- }
css={mobileNavigationButton}
label="Followers"
icon={}
/>
- navigate(`/profile/${username}/stars`)
- }
css={mobileNavigationButton}
label="Stars"
icon={}
/>
+ }
+ />
)}
diff --git a/src/components/profile/project-modal.tsx b/src/components/profile/project-modal.tsx
index 404e49f2..2c4f9fdb 100644
--- a/src/components/profile/project-modal.tsx
+++ b/src/components/profile/project-modal.tsx
@@ -1,9 +1,8 @@
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
import { useDispatch } from "@root/store";
import Tooltip from "@mui/material/Tooltip";
import SVGPaths from "@elem/svg-icons";
import ProjectAvatar from "@elem/project-avatar";
-import { IProject } from "@comp/projects/types";
import { SliderPicker } from "react-color";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
@@ -14,97 +13,260 @@ import styled from "@emotion/styled";
import IconButton from "@mui/material/IconButton";
import ReactAutosuggestExample from "./tag-auto-suggest";
import { isEmpty } from "ramda";
-import { addUserProject, editUserProject } from "./actions";
+import {
+ addUserProject,
+ editUserProject,
+ PROJECT_STARTER_TEMPLATE_OPTIONS,
+ ProjectStarterTemplate
+} from "./actions";
import { openSnackbar } from "../snackbar/actions";
import { SnackbarType } from "../snackbar/types";
import { closeModal } from "../modal/actions";
const avatarContainer = css`
- margin-left: -10px;
- margin-top: -10px;
- width: 62px;
- height: 62px;
- padding: 14px;
- -webkit-box-pack: center;
+ width: 88px;
+ height: 88px;
+ padding: 16px;
+ border-radius: 18px;
+ display: flex;
+ align-items: center;
justify-content: center;
cursor: pointer;
+ background: rgb(0 0 0 / 10%);
+ border: 1px solid rgb(255 255 255 / 10%);
+
.project-avatar {
position: relative;
border-radius: 50%;
+ transform: scale(1.15);
}
`;
const ModalContainer = styled.div`
- display: grid;
- grid-auto-rows: minmax(72px, auto);
- grid-template-columns: min(400px, calc(100vw - 24px));
- border-radius: 5px;
- box-sizing: border-box;
- padding: 6px 2px;
- row-gap: 6px;
-
- h2 {
- margin: 0;
- line-height: 1.3;
- }
+ width: min(520px, calc(100vw - 24px));
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
@media (max-width: 760px) {
- grid-template-columns: calc(100vw - 24px);
- grid-auto-rows: minmax(58px, auto);
- row-gap: 8px;
- padding: 8px 2px;
- border-radius: 8px;
+ width: calc(100vw - 24px);
+ gap: 14px;
}
`;
-interface IFieldRow {
- row: number;
-}
-const FieldRow = styled.div`
- grid-row: ${(properties) => properties.row};
- grid-column: 1;
+
+const HeaderBlock = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+`;
+
+const HeaderEyebrow = styled.span`
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ opacity: 0.68;
+`;
+
+const HeaderTitle = styled.h2`
+ margin: 0;
+ line-height: 1.15;
+ font-size: 28px;
@media (max-width: 760px) {
- padding-left: 2px;
- padding-right: 2px;
+ font-size: 24px;
}
`;
-const IconPickerContainer = styled.div`
+const HeaderBody = styled.p`
+ margin: 0;
+ line-height: 1.5;
+ opacity: 0.82;
+ font-size: 14px;
+`;
+
+const StepRail = styled.div`
display: grid;
- position: relative;
- grid-template-columns: 1fr 1fr 1fr;
- grid-template-rows: 1fr;
- grid-gap: 10px;
- padding: 4px;
- align-items: center;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px;
+`;
- @media (max-width: 760px) {
- grid-template-columns: 1fr;
- grid-template-rows: auto auto auto;
- grid-gap: 12px;
- justify-items: stretch;
+const StepCard = styled.button<{ active: boolean; complete: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ padding: 10px 12px;
+ border-radius: 12px;
+ border: 1px solid
+ ${(properties) =>
+ properties.active
+ ? "rgb(255 255 255 / 24%)"
+ : properties.complete
+ ? "rgb(255 255 255 / 12%)"
+ : "rgb(255 255 255 / 8%)"};
+ background: ${(properties) =>
+ properties.active ? "rgb(255 255 255 / 8%)" : "rgb(0 0 0 / 7%)"};
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+
+ &:disabled {
+ cursor: default;
+ opacity: 1;
}
`;
-const StyledSketchPicker = styled(SliderPicker)`
- grid-column: 2;
- grid-row: 1;
+const StepIndex = styled.span`
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ opacity: 0.66;
+`;
- @media (max-width: 760px) {
- grid-column: 1;
- grid-row: 2;
- width: 100% !important;
- max-width: 100%;
+const StepLabel = styled.span`
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1.3;
+`;
+
+const Section = styled.section`
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 14px;
+ border-radius: 14px;
+ background: rgb(0 0 0 / 7%);
+ border: 1px solid rgb(255 255 255 / 8%);
+`;
+
+const SectionTitle = styled.h3`
+ margin: 0;
+ font-size: 15px;
+`;
+
+const SectionCaption = styled.p`
+ margin: 0;
+ line-height: 1.45;
+ font-size: 13px;
+ opacity: 0.76;
+`;
+
+const FieldStack = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+`;
+
+const fieldSx = {
+ "& .MuiFormHelperText-root": {
+ color: "rgb(224 230 247 / 78%)",
+ marginLeft: 0,
+ marginRight: 0
+ },
+ "& .MuiInputLabel-root": {
+ color: "rgb(224 230 247 / 84%)"
+ },
+ "& .MuiInputLabel-root.Mui-focused": {
+ color: "rgb(248 250 255 / 92%)"
+ }
+};
+
+const StarterTemplateContainer = styled.div`
+ display: grid;
+ gap: 8px;
+
+ .MuiFormControlLabel-root {
+ align-items: flex-start;
+ margin: 0;
+ padding: 10px 12px;
+ border-radius: 12px;
+ background: rgb(0 0 0 / 7%);
+ border: 1px solid transparent;
+ transition:
+ border-color 120ms ease,
+ background-color 120ms ease;
+ }
+
+ .MuiFormControlLabel-root.selected {
+ border-color: rgb(255 255 255 / 24%);
+ background: rgb(255 255 255 / 6%);
+ }
+
+ .MuiFormControlLabel-label {
+ display: grid;
+ gap: 3px;
+ }
+`;
+
+const TemplateDescription = styled.span`
+ opacity: 0.76;
+ font-size: 12px;
+ line-height: 1.4;
+`;
+
+const TemplateGuide = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 12px;
+ border-radius: 12px;
+ background: rgb(255 255 255 / 4%);
+ border: 1px solid rgb(255 255 255 / 6%);
+`;
+
+const TemplateGuideTitle = styled.span`
+ font-size: 13px;
+ font-weight: 700;
+`;
+
+const TemplateGuideLine = styled.span`
+ font-size: 12px;
+ line-height: 1.45;
+ opacity: 0.76;
+`;
+
+const IconPickerContainer = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+
+ @media (min-width: 760px) {
+ grid-template-columns: 110px minmax(0, 1fr) 130px;
+ align-items: center;
}
`;
+const AvatarPreviewCard = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: center;
+ text-align: center;
+`;
+
+const AvatarPreviewHint = styled.span`
+ font-size: 12px;
+ opacity: 0.76;
+ line-height: 1.4;
+`;
+
+const StyledSketchPicker = styled(SliderPicker)`
+ width: 100% !important;
+ max-width: 100%;
+`;
+
const RadioGroupContainer = styled.div`
- grid-column: 3;
- grid-row: 1;
+ display: flex;
+ align-items: center;
- @media (max-width: 760px) {
- grid-column: 1;
- grid-row: 3;
+ .MuiRadioGroup-root {
+ width: 100%;
+ }
+
+ .MuiFormControlLabel-root {
+ margin-left: 0;
+ margin-right: 0;
}
`;
@@ -120,6 +282,29 @@ const PopoverContainer = styled.div`
}
`;
+const FooterActions = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+`;
+
+const FooterLeadingActions = styled.div`
+ display: flex;
+ gap: 10px;
+ flex-direction: column;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ }
+`;
+
+type WizardStep = 0 | 1 | 2;
+
interface IProjectModal {
name: string;
description: string;
@@ -129,9 +314,41 @@ interface IProjectModal {
iconBackgroundColor: string | undefined;
iconName: string | undefined;
newProject: boolean;
+ starterTemplate?: ProjectStarterTemplate;
}
+const templateGuides: Record<
+ ProjectStarterTemplate,
+ { title: string; lines: string[] }
+> = {
+ "single-csd": {
+ title: "Best for quick starts",
+ lines: [
+ "One self-contained file.",
+ "Good for teaching, sketching, and fast edits.",
+ "Opens with a ready-to-play Csound 7 example."
+ ]
+ },
+ "split-csd": {
+ title: "Best for structured projects",
+ lines: [
+ "Keeps wrapper, orchestra, and score separate.",
+ "Still starts from the same new default example.",
+ "Useful when you want students to compare roles of each file."
+ ]
+ },
+ empty: {
+ title: "Best for blank starts",
+ lines: [
+ "Creates an empty project.csd.",
+ "No starter notes or comments.",
+ "Useful when you already know the structure you want."
+ ]
+ }
+};
+
export const ProjectModal = (properties: IProjectModal) => {
+ const dispatch = useDispatch();
const [name, setName] = useState(properties.name);
const [description, setDescription] = useState(properties.description);
const [iconName, setIconName] = useState(properties.iconName);
@@ -142,47 +359,61 @@ export const ProjectModal = (properties: IProjectModal) => {
const [iconBackgroundColor, setIconBackgroundColor] = useState(
properties.iconBackgroundColor || "#000"
);
-
+ const [starterTemplate, setStarterTemplate] =
+ useState(
+ properties.starterTemplate || "single-csd"
+ );
const [popupState, setPopupState] = useState(false);
const [anchorElement, setAnchorElement] = useState(
null as HTMLSpanElement | null
);
- const dispatch = useDispatch();
- // const currentTags = useSelector(selectTags(properties.projectID));
- const [modifiedTags, setModifiedTags] = useState([]);
- const shouldDisable = isEmpty(name);
+ const [modifiedTags, setModifiedTags] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [step, setStep] = useState(0);
- // useEffect(() => {
- // if (not(equals(currentTags, modifiedTags)) && !isEmpty(currentTags)) {
- // setModifiedTags(currentTags);
- // }
- // // eslint-disable-next-line react-hooks/exhaustive-deps
- // }, [currentTags]);
+ const shouldDisable = isEmpty(name.trim()) || isSubmitting;
+ const isWizard = properties.newProject;
+
+ const activeTemplateGuide = useMemo(
+ () => templateGuides[starterTemplate],
+ [starterTemplate]
+ );
const handleOnSubmit = async () => {
+ if (shouldDisable) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
try {
- dispatch(
- properties.newProject
- ? addUserProject(
- name,
- description,
- modifiedTags,
- properties.projectID,
- iconName || "default",
- iconForegroundColor || "#000000",
- iconBackgroundColor || "#FFF"
- )
- : editUserProject(
- name,
- description,
- modifiedTags,
- properties.projectID,
- iconName || "default",
- iconForegroundColor || "#000000",
- iconBackgroundColor || "#FFF"
- )
- );
- dispatch(closeModal());
+ if (properties.newProject) {
+ await dispatch(
+ addUserProject(
+ name.trim(),
+ description,
+ modifiedTags,
+ properties.projectID,
+ iconName || "default",
+ iconForegroundColor || "#000000",
+ iconBackgroundColor || "#FFF",
+ starterTemplate
+ ) as any
+ );
+ } else {
+ await dispatch(
+ editUserProject(
+ name.trim(),
+ description,
+ modifiedTags,
+ properties.projectID,
+ iconName || "default",
+ iconForegroundColor || "#000000",
+ iconBackgroundColor || "#FFF"
+ ) as any
+ );
+ dispatch(closeModal());
+ }
} catch (error) {
dispatch(
openSnackbar(
@@ -190,9 +421,10 @@ export const ProjectModal = (properties: IProjectModal) => {
SnackbarType.Error
)
);
+ } finally {
+ setIsSubmitting(false);
}
};
- const textFieldStyle = { marginBottom: 12 };
const handleProfileDropDown = (
event: React.MouseEvent
@@ -206,167 +438,320 @@ export const ProjectModal = (properties: IProjectModal) => {
setPopupState(false);
};
+ const stepMeta = [
+ { label: "Project details" },
+ { label: "Starter template" },
+ { label: "Visual identity" }
+ ] as const;
+
+ const goToStep = (nextStep: WizardStep) => {
+ if (!isWizard) {
+ return;
+ }
+ setStep(nextStep);
+ };
+
return (
-
- {properties.newProject ? (
- Please Name Your Project
- ) : (
- {`Editing "${name}"`}
- )}
-
-
-
- {
- setName(event.target.value);
- }}
- fullWidth
- />
-
-
- {
- setDescription(event.target.value);
- }}
- margin="normal"
- fullWidth
- />
-
-
-
-
-
-
-
-
+
+ {properties.newProject ? "New Project" : "Project Settings"}
+
+
+ {properties.newProject
+ ? "Create a project"
+ : `Edit ${name || "project"}`}
+
+
+ {properties.newProject
+ ? "Set up the project in three short steps, then jump into the editor."
+ : "Update the name, description, tags, and icon for this project."}
+
+
+
+ {isWizard && (
+
+ {stepMeta.map((item, index) => (
+ index}
+ onClick={() => goToStep(index as WizardStep)}
>
-
-
-
-
-
-
- {Array.isArray(Object.entries(SVGPaths)) &&
- Object.entries(SVGPaths).map(
- (entry, index) => {
- const SvgElem: any =
- entry[1] as any;
-
- return (
-
- {
- setIconName(
- entry[0]
- );
- handlePopoverClose();
- }}
- >
-
-
-
- );
- }
- )}
-
-
-
- {
- if (foregroundColor) {
- setIconForegroundColor(event.hex);
- } else {
- setIconBackgroundColor(event.hex);
+ {`Step ${index + 1}`}
+ {item.label}
+
+ ))}
+
+ )}
+
+ {(!isWizard || step === 0) && (
+
+ Project details
+ Start with the basics.
+
+
-
+ value={name}
+ onChange={(event) => {
+ setName(event.target.value);
+ }}
+ fullWidth
+ sx={fieldSx}
+ />
+ {
+ setDescription(event.target.value);
+ }}
+ fullWidth
+ sx={fieldSx}
+ />
+
+
+
+ )}
+
+ {properties.newProject && step === 1 && (
+
+ Starter template
+
+ Choose the file structure before opening the editor.
+
+
{
- setIsForegroundColor(
- event.target.value === "foreground"
- ? true
- : false
+ setStarterTemplate(
+ event.target.value as ProjectStarterTemplate
);
}}
>
- }
- label="Foreground"
- />
- }
- label="Background"
- />
+ {PROJECT_STARTER_TEMPLATE_OPTIONS.map((option) => (
+ }
+ label={
+ <>
+ {option.label}
+
+ {option.description}
+
+ >
+ }
+ />
+ ))}
-
-
-
-
-
-
+
+
+
+ {activeTemplateGuide.title}
+
+ {activeTemplateGuide.lines.map((line) => (
+
+ {line}
+
+ ))}
+
+
+ )}
+
+ {(!isWizard || step === 2) && (
+
+ Visual identity
+
+ Choose an icon and colors that are easy to recognize.
+
+
+
+
+
+
+
+
+
+ Tap the avatar to browse icons.
+
+
+
+
+
+
+ {Array.isArray(Object.entries(SVGPaths)) &&
+ Object.entries(SVGPaths).map(
+ (entry, index) => {
+ const SvgElem: any =
+ entry[1] as any;
+
+ return (
+
+ {
+ setIconName(
+ entry[0]
+ );
+ handlePopoverClose();
+ }}
+ >
+
+
+
+ );
+ }
+ )}
+
+
+
+
+ {
+ if (foregroundColor) {
+ setIconForegroundColor(event.hex);
+ } else {
+ setIconBackgroundColor(event.hex);
+ }
+ }}
+ />
+
+
+ {
+ setIsForegroundColor(
+ event.target.value === "foreground"
+ );
+ }}
+ >
+ }
+ label="Edit foreground"
+ />
+ }
+ label="Edit background"
+ />
+
+
+
+
+ )}
+
+
+
+
+ {isWizard && step > 0 && (
+
+ )}
+
+ {isWizard && step < 2 ? (
+
+ ) : (
+
+ )}
+
);
};
diff --git a/src/components/profile/selectors.ts b/src/components/profile/selectors.ts
index ffe0bf7f..13c361ab 100644
--- a/src/components/profile/selectors.ts
+++ b/src/components/profile/selectors.ts
@@ -4,33 +4,40 @@ import { IProfileReducer } from "./reducer";
import { path, pathOr } from "ramda";
// import Fuse from "fuse.js";
import { IProject } from "../projects/types";
+import { IProfile } from "./types";
-export const selectUserFollowing =
- (profileUid: string | undefined): ((store: RootState) => Array) =>
- (store: RootState) => {
- if (profileUid) {
- const state: IProfileReducer = store.ProfileReducer;
- return pathOr([], ["profiles", profileUid, "following"], state);
- } else {
- return [];
- }
- };
+const EMPTY_STRING_ARRAY: string[] = [];
+const EMPTY_PROJECT_ARRAY: IProject[] = [];
+const EMPTY_PROJECTS_COUNT = {
+ all: 0,
+ public: 0
+};
-export const selectUserProjects =
- (profileUid: string | undefined) => (store: RootState) => {
- if (profileUid) {
- const state = store.ProjectsReducer.projects;
+export const selectUserFollowing = (profileUid: string | undefined) =>
+ createSelector(
+ [(store: RootState) => store.ProfileReducer.profiles],
+ (profiles) => {
+ if (!profileUid) {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ return profiles[profileUid]?.following ?? EMPTY_STRING_ARRAY;
+ }
+ );
+
+export const selectUserProjects = (profileUid: string | undefined) =>
+ createSelector(
+ [(store: RootState) => store.ProjectsReducer.projects],
+ (projects) => {
+ if (!profileUid) {
+ return EMPTY_PROJECT_ARRAY;
+ }
- // Filter projects by matching `userUid` with `profileUid`
- const filteredProjects = Object.values(state).filter(
+ return Object.values(projects).filter(
(project) => project.userUid === profileUid
);
-
- return filteredProjects;
- } else {
- return [];
}
- };
+ );
export const selectProjectFilterString = (
store: RootState
@@ -47,7 +54,8 @@ export const selectFollowingFilterString = (
};
export const selectUserProfile =
- (profileUid: string | undefined) => (store: RootState) => {
+ (profileUid: string | undefined) =>
+ (store: RootState): IProfile | undefined => {
if (profileUid) {
const state: IProfileReducer = store.ProfileReducer;
return state.profiles[profileUid];
@@ -76,9 +84,13 @@ export const selectLoggedInUserStars = (store: RootState): Array => {
const loggedInUid: string | undefined = store.LoginReducer.loggedInUid;
if (loggedInUid) {
const state: IProfileReducer = store.ProfileReducer;
- return pathOr([], ["profiles", loggedInUid, "starred"], state);
+ return pathOr(
+ EMPTY_STRING_ARRAY,
+ ["profiles", loggedInUid, "starred"],
+ state
+ );
} else {
- return [];
+ return EMPTY_STRING_ARRAY;
}
};
@@ -135,15 +147,15 @@ export const selectLoggedInUserStars = (store: RootState): Array => {
// )(store);
export const selectFilteredUserFollowers =
- (profileUid: string): ((store: RootState) => any) =>
+ (profileUid: string): ((store: RootState) => Array) =>
(store) => {
const state: IProfileReducer = store.ProfileReducer;
const followerUids = pathOr(
- [],
+ EMPTY_STRING_ARRAY,
["profiles", profileUid, "followers"],
state
);
- return (followerUids || []).map((followerUid) =>
+ return followerUids.map((followerUid) =>
path(["profiles", followerUid], state)
);
};
@@ -171,23 +183,20 @@ export const selectCurrentTagText = (store: RootState): string => {
};
export const selectTags =
- (projectUid: string): ((store: RootState) => any) =>
+ (projectUid: string): ((store: RootState) => string[]) =>
(store) => {
return pathOr(
- [],
+ EMPTY_STRING_ARRAY,
["ProjectsReducer", "projects", projectUid, "tags"],
store
);
};
export const selectProfileStars =
- (profileUid: string) => (store: RootState) => {
- return pathOr(
- [],
- ["ProfileReducer", "profiles", profileUid, "stars"],
- store
- );
- };
+ (profileUid: string) =>
+ (store: RootState): string[] =>
+ store.ProfileReducer?.profiles?.[profileUid]?.stars ??
+ EMPTY_STRING_ARRAY;
export const selectAllUserProjectUids =
(profileUid: string | undefined) => (store: RootState) => {
@@ -201,42 +210,31 @@ export const selectAllUserProjectUids =
return allUserProjects.map((p) => p.projectUid);
} else {
- return [];
+ return EMPTY_STRING_ARRAY;
}
};
export const selectAllTagsFromUser =
- (profileUid: string): ((store: RootState) => any) =>
+ (profileUid: string): ((store: RootState) => string[]) =>
(store) => {
return pathOr(
- [],
+ EMPTY_STRING_ARRAY,
["ProfileReducer", "profiles", profileUid, "allTags"],
store
);
};
-// Selector to get profileUid (customize this as per your state structure)
-export const selectProfileUid = createSelector(
- [(state: RootState) => state.LoginReducer.loggedInUid], // Replace with the correct state slice
- (loggedInUid) => loggedInUid
-);
+export const selectProfileUid = (state: RootState): string | undefined =>
+ state.LoginReducer.loggedInUid;
// Selector to get the projectsCount for the logged-in user
export const selectProfileProjectsCount = createSelector(
[selectProfiles, selectProfileUid],
(profiles, profileUid) => {
if (!profileUid || !profiles[profileUid]) {
- return {
- all: 0,
- public: 0
- };
+ return EMPTY_PROJECTS_COUNT;
}
- return (
- profiles[profileUid].projectsCount ?? {
- all: 0,
- public: 0
- }
- );
+ return profiles[profileUid].projectsCount ?? EMPTY_PROJECTS_COUNT;
}
);
@@ -244,24 +242,13 @@ export const selectProfileProjectsCount = createSelector(
export const selectUserProjectsCount =
(profileUid: string | undefined) => (store: RootState) => {
if (!profileUid) {
- return {
- all: 0,
- public: 0
- };
+ return EMPTY_PROJECTS_COUNT;
}
const profiles = selectProfiles(store);
if (!profiles[profileUid]) {
- return {
- all: 0,
- public: 0
- };
+ return EMPTY_PROJECTS_COUNT;
}
- return (
- profiles[profileUid].projectsCount ?? {
- all: 0,
- public: 0
- }
- );
+ return profiles[profileUid].projectsCount ?? EMPTY_PROJECTS_COUNT;
};
// export const selectProjectIconStyle = (
diff --git a/src/components/profile/tabs/stars-list.tsx b/src/components/profile/tabs/stars-list.tsx
index 6e9f81a5..040e01c3 100644
--- a/src/components/profile/tabs/stars-list.tsx
+++ b/src/components/profile/tabs/stars-list.tsx
@@ -13,12 +13,14 @@ import {
Box,
CircularProgress
} from "@mui/material";
-import { useSelector } from "react-redux";
+import { useSelector, shallowEqual } from "react-redux";
import { useNavigate } from "react-router";
import { isEmpty } from "ramda";
import StarIcon from "@mui/icons-material/Star";
import * as SS from "./styles";
+const EMPTY_STRING_ARRAY: string[] = [];
+
export const StarsList = ({
profileUid,
isLoading = false
@@ -27,7 +29,12 @@ export const StarsList = ({
isLoading?: boolean;
}) => {
const navigate = useNavigate();
- const profileStars = useSelector(selectProfileStars(profileUid));
+ const profileStars = useSelector(
+ (store: RootState): string[] =>
+ store.ProfileReducer?.profiles?.[profileUid]?.stars ??
+ EMPTY_STRING_ARRAY,
+ shallowEqual
+ );
const cachedProjects = useSelector(
(store: RootState) => store.ProjectsReducer.projects
);
diff --git a/src/components/profile/types.ts b/src/components/profile/types.ts
index 2b175fe6..185c5207 100644
--- a/src/components/profile/types.ts
+++ b/src/components/profile/types.ts
@@ -59,11 +59,11 @@ export type ProfileActionTypes = {
};
export interface IProfile {
- allTags?: any[];
+ allTags?: string[];
bio?: string;
userUid: string;
- userFollowing?: [];
- followers?: [];
+ following?: string[];
+ followers?: string[];
projectsCount?: ProjectsCount;
userImageURL?: string;
backgroundIndex: number;
@@ -75,4 +75,5 @@ export interface IProfile {
username: string;
photoUrl?: string;
userJoinDate?: number;
+ stars?: string[];
}
diff --git a/src/csound-templates/default-split.csd b/src/csound-templates/default-split.csd
new file mode 100644
index 00000000..fb9722af
--- /dev/null
+++ b/src/csound-templates/default-split.csd
@@ -0,0 +1,11 @@
+
+
+-o dac ; real-time audio output
+
+
+#include "project.orc"
+
+
+#include "project.sco"
+
+
\ No newline at end of file
diff --git a/src/csound-templates/default.csd b/src/csound-templates/default.csd
index 06b45e78..86a802ef 100644
--- a/src/csound-templates/default.csd
+++ b/src/csound-templates/default.csd
@@ -1,20 +1,27 @@
+-o dac ; real-time audio output
-0dbfs=1
-nchnls=2
+sr = 44100 ; sample rate
+0dbfs = 1 ; full-scale amplitude
+nchnls = 2 ; stereo output
+ksmps = 64 ; control rate block size
+; Instrument 1 plays a simple sawtooth tone.
+; p4 is amplitude and p5 is frequency.
instr 1
- asig vco2 p4, p5
- aenv linenr asig,0.01,0.1,0.01
- out aenv, aenv
+ iAmp = p4
+ iFreq = p5
+ aOut = vco2:a(iAmp, iFreq)
+ outall(aOut)
endin
-event_i "i", 1, 0, 2, 0dbfs/2, A4
-event_i "e", 0, 2.5
-
+; A short "hello world" phrase for new projects.
+i 1 0 0.35 0.10 440
+i 1 0.45 0.35 0.08 660
+i 1 0.95 0.60 0.06 550
\ No newline at end of file
diff --git a/src/csound-templates/default.orc b/src/csound-templates/default.orc
index ab81e7ef..ca2a2f4e 100644
--- a/src/csound-templates/default.orc
+++ b/src/csound-templates/default.orc
@@ -1,16 +1,13 @@
-sr=44100
-ksmps=32
-0dbfs=1
-nchnls=2
+sr = 44100 ; sample rate
+0dbfs = 1 ; full-scale amplitude
+nchnls = 2 ; stereo output
+ksmps = 64 ; control rate block size
+; Instrument 1 plays a simple sawtooth tone.
+; p4 is amplitude and p5 is frequency.
instr 1
-
- iamp = ampdbfs(p5)
- ipch = cps2pch(p4,12)
- ipan = 0.5
-
- asig = vco2(iamp, ipch)
- al, ar pan2 asig, ipan
-
- out(al, ar)
+ iAmp = p4
+ iFreq = p5
+ aOut = vco2:a(iAmp, iFreq)
+ outall(aOut)
endin
\ No newline at end of file
diff --git a/src/csound-templates/default.sco b/src/csound-templates/default.sco
index a869164e..48b24c40 100644
--- a/src/csound-templates/default.sco
+++ b/src/csound-templates/default.sco
@@ -1 +1,4 @@
-i1 0 2 8.00 -12
\ No newline at end of file
+; A short "hello world" phrase for new projects.
+i 1 0 0.35 0.10 440
+i 1 0.45 0.35 0.08 660
+i 1 0.95 0.60 0.06 550
\ No newline at end of file
diff --git a/src/csound-templates/index.js b/src/csound-templates/index.js
index 238e0f34..0d308495 100644
--- a/src/csound-templates/index.js
+++ b/src/csound-templates/index.js
@@ -1,4 +1,5 @@
import * as defaultCsdTxt from "./default.csd?raw";
+import * as defaultSplitCsdTxt from "./default-split.csd?raw";
import * as defaultOrcTxt from "./default.orc?raw";
import * as defaultScoTxt from "./default.sco?raw";
@@ -8,6 +9,12 @@ export const defaultCsd = {
type: "txt"
};
+export const defaultSplitCsd = {
+ name: "project.csd",
+ value: defaultSplitCsdTxt.default,
+ type: "txt"
+};
+
export const defaultOrc = {
name: "project.orc",
value: defaultOrcTxt.default,
@@ -19,3 +26,9 @@ export const defaultSco = {
value: defaultScoTxt.default,
type: "txt"
};
+
+export const emptyCsd = {
+ name: "project.csd",
+ value: "",
+ type: "txt"
+};