feat: Implement collaboration features including user following and avatar management
This commit is contained in:
parent
ea53af62c4
commit
c1a7fe3015
|
@ -2,9 +2,9 @@ import React, { useEffect, useState } from "react";
|
||||||
import { AppDockIcon } from "../../icons/HeaderIcons";
|
import { AppDockIcon } from "../../icons/HeaderIcons";
|
||||||
import orgImg from "../../../assets/orgTemp.png";
|
import orgImg from "../../../assets/orgTemp.png";
|
||||||
import { useActiveUsers } from "../../../store/store";
|
import { useActiveUsers } from "../../../store/store";
|
||||||
import { getAvatarColor } from "../../../functions/users/functions/getAvatarColor";
|
|
||||||
import { ActiveUser } from "../../../types/users";
|
import { ActiveUser } from "../../../types/users";
|
||||||
import CollaborationPopup from "../../templates/CollaborationPopup";
|
import CollaborationPopup from "../../templates/CollaborationPopup";
|
||||||
|
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const { activeUsers } = useActiveUsers();
|
const { activeUsers } = useActiveUsers();
|
||||||
|
|
|
@ -49,6 +49,9 @@ const SideBarRight: React.FC = () => {
|
||||||
setSubModule("simulations");
|
setSubModule("simulations");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (activeModule !== "simulation") {
|
||||||
|
setSubModule("properties");
|
||||||
|
}
|
||||||
}, [activeModule, selectedEventData, selectedEventSphere, setSubModule]);
|
}, [activeModule, selectedEventData, selectedEventSphere, setSubModule]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -282,8 +282,7 @@ const GlobalProperties: React.FC = () => {
|
||||||
key={"6"}
|
key={"6"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="split"></div>
|
{/* <div className="split"></div>
|
||||||
|
|
||||||
<InputToggle
|
<InputToggle
|
||||||
inputKey="6"
|
inputKey="6"
|
||||||
label="Display Grid"
|
label="Display Grid"
|
||||||
|
@ -301,7 +300,7 @@ const GlobalProperties: React.FC = () => {
|
||||||
max={5}
|
max={5}
|
||||||
onChange={(value: number) => updateGridDistance(value)}
|
onChange={(value: number) => updateGridDistance(value)}
|
||||||
onPointerUp={updatedGrid}
|
onPointerUp={updatedGrid}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from "react";
|
||||||
|
import RenderOverlay from "./Overlay";
|
||||||
|
import { useSelectedUserStore } from "../../store/useCollabStore";
|
||||||
|
import { useCamMode } from "../../store/store";
|
||||||
|
|
||||||
|
const FollowPerson: React.FC = () => {
|
||||||
|
// Get the selected user from the store
|
||||||
|
const { selectedUser, clearSelectedUser } = useSelectedUserStore();
|
||||||
|
const { setCamMode } = useCamMode();
|
||||||
|
return (
|
||||||
|
<RenderOverlay>
|
||||||
|
{selectedUser && (
|
||||||
|
// eslint-disable-next-line
|
||||||
|
<div
|
||||||
|
className="follow-person-container"
|
||||||
|
onClick={() => {
|
||||||
|
clearSelectedUser();
|
||||||
|
setCamMode("FirstPerson");
|
||||||
|
}}
|
||||||
|
style={{ "--user-color": selectedUser.color } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<div className="follower-name">{selectedUser.name}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RenderOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FollowPerson;
|
|
@ -1,31 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import CustomAvatar from "./users/Avatar";
|
|
||||||
|
|
||||||
interface CollabUserIconProps {
|
|
||||||
userName: string;
|
|
||||||
userImage?: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CollabUserIcon: React.FC<CollabUserIconProps> = ({
|
|
||||||
userImage,
|
|
||||||
userName,
|
|
||||||
color,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="collab-user-live-container">
|
|
||||||
<div className="user-image-container">
|
|
||||||
{userImage ? (
|
|
||||||
<img className="user-image" src={userImage} alt={userName} />
|
|
||||||
) : (
|
|
||||||
<CustomAvatar name={userName} color={color} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="user-name" style={{ backgroundColor: color }}>
|
|
||||||
{userName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollabUserIcon;
|
|
|
@ -8,9 +8,9 @@ import { useActiveUsers, useSocketStore } from "../../../store/store";
|
||||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Html } from "@react-three/drei";
|
import { Html } from "@react-three/drei";
|
||||||
import CollabUserIcon from "../../../functions/collabUserIcon";
|
import CollabUserIcon from "./collabUserIcon";
|
||||||
import { getAvatarColor } from "../../../functions/users/functions/getAvatarColor";
|
|
||||||
import useModuleStore from "../../../store/useModuleStore";
|
import useModuleStore from "../../../store/useModuleStore";
|
||||||
|
import { getAvatarColor } from "../functions/getAvatarColor";
|
||||||
|
|
||||||
const CamModelsGroup = () => {
|
const CamModelsGroup = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -20,13 +20,14 @@ const CamModelsGroup = () => {
|
||||||
const { socket } = useSocketStore();
|
const { socket } = useSocketStore();
|
||||||
const { activeModule } = useModuleStore();
|
const { activeModule } = useModuleStore();
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader();
|
||||||
const dracoLoader = new DRACOLoader();
|
const dracoLoader = new DRACOLoader();
|
||||||
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
|
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
|
||||||
loader.setDRACOLoader(dracoLoader);
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
|
||||||
const [cams, setCams] = useState<any[]>([]);
|
const [cams, setCams] = useState<any[]>([]);
|
||||||
const [models, setModels] = useState<Record<string, { targetPosition: THREE.Vector3; targetRotation: THREE.Euler }>>({});
|
const [models, setModels] = useState<Record<string, { targetPosition: THREE.Vector3; targetRotation: THREE.Euler, target: THREE.Vector3 }>>({});
|
||||||
|
|
||||||
const dedupeCams = (cams: any[]) => {
|
const dedupeCams = (cams: any[]) => {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
@ -102,6 +103,7 @@ const CamModelsGroup = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("cameraUpdateResponse", (data: any) => {
|
socket.on("cameraUpdateResponse", (data: any) => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!groupRef.current ||
|
!groupRef.current ||
|
||||||
socket.id === data.socketId ||
|
socket.id === data.socketId ||
|
||||||
|
@ -122,6 +124,11 @@ const CamModelsGroup = () => {
|
||||||
data.data.rotation.y,
|
data.data.rotation.y,
|
||||||
data.data.rotation.z
|
data.data.rotation.z
|
||||||
),
|
),
|
||||||
|
target: new THREE.Vector3(
|
||||||
|
data.data.target.x,
|
||||||
|
data.data.target.y,
|
||||||
|
data.data.target.z
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -131,7 +138,7 @@ const CamModelsGroup = () => {
|
||||||
socket.off("userDisConnectResponse");
|
socket.off("userDisConnectResponse");
|
||||||
socket.off("cameraUpdateResponse");
|
socket.off("cameraUpdateResponse");
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [email, loader, navigate, setActiveUsers, socket]);
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (!groupRef.current) return;
|
if (!groupRef.current) return;
|
||||||
|
@ -217,9 +224,11 @@ const CamModelsGroup = () => {
|
||||||
position={[-0.015, 0, 0.7]}
|
position={[-0.015, 0, 0.7]}
|
||||||
>
|
>
|
||||||
<CollabUserIcon
|
<CollabUserIcon
|
||||||
userImage={cam.userData.userImage || ""}
|
userImage={cam.userData.userImage ?? ""}
|
||||||
userName={cam.userData.userName}
|
userName={cam.userData.userName}
|
||||||
color={getAvatarColor(index, cam.userData.userName)}
|
color={getAvatarColor(index, cam.userData.userName)}
|
||||||
|
position={cam.position}
|
||||||
|
rotation={cam.rotation}
|
||||||
/>
|
/>
|
||||||
</Html>
|
</Html>
|
||||||
</primitive>
|
</primitive>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React from "react";
|
||||||
|
import CustomAvatar from "../users/Avatar";
|
||||||
|
import { useSelectedUserStore } from "../../../store/useCollabStore";
|
||||||
|
import { useCamMode } from "../../../store/store";
|
||||||
|
|
||||||
|
interface CollabUserIconProps {
|
||||||
|
userName: string;
|
||||||
|
userImage?: string;
|
||||||
|
color: string;
|
||||||
|
position?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
rotation?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollabUserIcon: React.FC<CollabUserIconProps> = ({
|
||||||
|
userImage,
|
||||||
|
userName,
|
||||||
|
color,
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
}) => {
|
||||||
|
const { setSelectedUser } = useSelectedUserStore();
|
||||||
|
const { setCamMode } = useCamMode();
|
||||||
|
return (
|
||||||
|
<div className="collab-user-live-container">
|
||||||
|
<button
|
||||||
|
className="user-image-container"
|
||||||
|
onClick={() => {
|
||||||
|
if(!position || !rotation) return;
|
||||||
|
|
||||||
|
// Set the selected user in the store
|
||||||
|
setSelectedUser({ color: color, name: userName, location: { position, rotation } });
|
||||||
|
setCamMode("FollowPerson");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userImage ? (
|
||||||
|
<img className="user-image" src={userImage} alt={userName} />
|
||||||
|
) : (
|
||||||
|
<CustomAvatar name={userName} color={color} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div className="user-name" style={{ backgroundColor: color }}>
|
||||||
|
{userName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollabUserIcon;
|
|
@ -1,14 +1,32 @@
|
||||||
import React from 'react'
|
import React, { useEffect } from "react";
|
||||||
import CamModelsGroup from './camera/collabCams'
|
import CamModelsGroup from "./camera/collabCams";
|
||||||
|
import { useSelectedUserStore } from "../../store/useCollabStore";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import setCameraView from "./functions/setCameraView";
|
||||||
|
import { useCamMode } from "../../store/store";
|
||||||
|
|
||||||
const Collaboration = () => {
|
const Collaboration: React.FC = () => {
|
||||||
return (
|
const { selectedUser } = useSelectedUserStore();
|
||||||
<>
|
const { camMode } = useCamMode();
|
||||||
|
const { camera, controls } = useThree(); // Access R3F camera and controls
|
||||||
|
|
||||||
<CamModelsGroup />
|
useEffect(() => {
|
||||||
|
if(camMode !== "FollowPerson") return;
|
||||||
|
// If a user is selected, set the camera view to their location
|
||||||
|
// and update the camera and controls accordingly
|
||||||
|
if (selectedUser?.location) {
|
||||||
|
const { position, rotation } = selectedUser.location;
|
||||||
|
setCameraView({
|
||||||
|
controls,
|
||||||
|
camera,
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
username: selectedUser.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedUser, camera, controls, camMode]);
|
||||||
|
|
||||||
</>
|
return <CamModelsGroup />;
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default Collaboration
|
export default Collaboration;
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function getAvatarColor(index: number, name?: string): string {
|
||||||
const localStorageKey = "userAvatarColors";
|
const localStorageKey = "userAvatarColors";
|
||||||
// Check if local storage is available
|
// Check if local storage is available
|
||||||
if (name) {
|
if (name) {
|
||||||
let userColors = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
|
let userColors = JSON.parse(localStorage.getItem(localStorageKey) ?? "{}");
|
||||||
|
|
||||||
// Check if the user already has an assigned color
|
// Check if the user already has an assigned color
|
||||||
if (userColors[name]) {
|
if (userColors[name]) {
|
|
@ -0,0 +1,46 @@
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
interface SetCameraViewProps {
|
||||||
|
controls: any;
|
||||||
|
camera: THREE.Camera;
|
||||||
|
position: THREE.Vector3 | { x: number; y: number; z: number };
|
||||||
|
rotation: THREE.Euler | { x: number; y: number; z: number };
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function setCameraView({
|
||||||
|
controls,
|
||||||
|
camera,
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
username,
|
||||||
|
}: SetCameraViewProps) {
|
||||||
|
if (!controls || !camera) return;
|
||||||
|
|
||||||
|
// Normalize position
|
||||||
|
const newPosition = position instanceof THREE.Vector3
|
||||||
|
? position
|
||||||
|
: new THREE.Vector3(position.x, position.y, position.z);
|
||||||
|
|
||||||
|
// Normalize rotation
|
||||||
|
const newRotation = rotation instanceof THREE.Euler
|
||||||
|
? rotation
|
||||||
|
: new THREE.Euler(rotation.x, rotation.y, rotation.z);
|
||||||
|
|
||||||
|
// Update camera position and rotation
|
||||||
|
// camera.position.copy(newPosition);
|
||||||
|
// camera.rotation.copy(newRotation);
|
||||||
|
|
||||||
|
// If your controls need to update the target, you can optionally adjust it too
|
||||||
|
if (controls.setTarget) {
|
||||||
|
// Setting a basic target slightly forward from new position based on rotation
|
||||||
|
const cameraDirection = new THREE.Vector3(0, 0, -1).applyEuler(newRotation);
|
||||||
|
const targetPosition = new THREE.Vector3().copy(newPosition).add(cameraDirection);
|
||||||
|
|
||||||
|
// controls.setTarget(targetPosition.x, targetPosition.y, targetPosition.z);
|
||||||
|
controls?.setLookAt(...newPosition.toArray(), newPosition.x, 0, newPosition.z, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally you can log
|
||||||
|
console.log(`Camera view updated by ${username ?? 'unknown user'}`);
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { getInitials } from "./functions/getInitials";
|
import { getInitials } from "../functions/getInitials";
|
||||||
import { getAvatarColor } from "./functions/getAvatarColor";
|
|
||||||
|
|
||||||
interface AvatarProps {
|
interface AvatarProps {
|
||||||
name: string; // Name can be a full name or initials
|
name: string; // Name can be a full name or initials
|
||||||
|
@ -26,7 +25,7 @@ const CustomAvatar: React.FC<AvatarProps> = ({
|
||||||
const initials = getInitials(name); // Convert name to initials if needed
|
const initials = getInitials(name); // Convert name to initials if needed
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
ctx.fillStyle = color || "#323232"; // Use color prop or generate color based on index
|
ctx.fillStyle = color ?? "#323232"; // Use color prop or generate color based on index
|
||||||
ctx.fillRect(0, 0, size, size);
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
|
||||||
// Draw initials
|
// Draw initials
|
||||||
|
@ -40,7 +39,7 @@ const CustomAvatar: React.FC<AvatarProps> = ({
|
||||||
const dataURL = canvas.toDataURL("image/png");
|
const dataURL = canvas.toDataURL("image/png");
|
||||||
setImageSrc(dataURL);
|
setImageSrc(dataURL);
|
||||||
}
|
}
|
||||||
}, [name, size, textColor]);
|
}, [color, name, size, textColor]);
|
||||||
|
|
||||||
if (!imageSrc) {
|
if (!imageSrc) {
|
||||||
return null; // Return null while the image is being generated
|
return null; // Return null while the image is being generated
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import ModuleToggle from "../components/ui/ModuleToggle";
|
import ModuleToggle from "../components/ui/ModuleToggle";
|
||||||
import SideBarLeft from "../components/layout/sidebarLeft/SideBarLeft";
|
import SideBarLeft from "../components/layout/sidebarLeft/SideBarLeft";
|
||||||
import SideBarRight from "../components/layout/sidebarRight/SideBarRight";
|
import SideBarRight from "../components/layout/sidebarRight/SideBarRight";
|
||||||
import useModuleStore, { useThreeDStore } from "../store/useModuleStore";
|
import useModuleStore, { useThreeDStore } from "../store/useModuleStore";
|
||||||
import RealTimeVisulization from "../modules/visualization/RealTimeVisulization";
|
import RealTimeVisulization from "../modules/visualization/RealTimeVisulization";
|
||||||
import Tools from "../components/ui/Tools";
|
import Tools from "../components/ui/Tools";
|
||||||
// import Scene from "../modules/scene/scene";
|
|
||||||
import {
|
import {
|
||||||
useSocketStore,
|
useSocketStore,
|
||||||
useFloorItems,
|
useFloorItems,
|
||||||
|
@ -20,12 +19,9 @@ import { usePlayButtonStore } from "../store/usePlayButtonStore";
|
||||||
import MarketPlace from "../modules/market/MarketPlace";
|
import MarketPlace from "../modules/market/MarketPlace";
|
||||||
import LoadingPage from "../components/templates/LoadingPage";
|
import LoadingPage from "../components/templates/LoadingPage";
|
||||||
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
|
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
|
||||||
import RenderOverlay from "../components/templates/Overlay";
|
|
||||||
import MenuBar from "../components/ui/menu/menu";
|
|
||||||
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
|
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
|
||||||
import ProductionCapacity from "../components/ui/analysis/ProductionCapacity";
|
import { useSelectedUserStore } from "../store/useCollabStore";
|
||||||
import ThroughputSummary from "../components/ui/analysis/ThroughputSummary";
|
import FollowPerson from "../components/templates/FollowPerson";
|
||||||
import ROISummary from "../components/ui/analysis/ROISummary";
|
|
||||||
|
|
||||||
const Project: React.FC = () => {
|
const Project: React.FC = () => {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
@ -44,7 +40,7 @@ const Project: React.FC = () => {
|
||||||
setActiveModule("builder");
|
setActiveModule("builder");
|
||||||
const email = localStorage.getItem("email");
|
const email = localStorage.getItem("email");
|
||||||
if (email) {
|
if (email) {
|
||||||
const Organization = email!.split("@")[1].split(".")[0];
|
const Organization = email.split("@")[1].split(".")[0];
|
||||||
useSocketStore.getState().initializeSocket(email, Organization);
|
useSocketStore.getState().initializeSocket(email, Organization);
|
||||||
const name = localStorage.getItem("userName");
|
const name = localStorage.getItem("userName");
|
||||||
if (Organization && name) {
|
if (Organization && name) {
|
||||||
|
@ -55,8 +51,10 @@ const Project: React.FC = () => {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isPlaying } = usePlayButtonStore();
|
const { isPlaying } = usePlayButtonStore();
|
||||||
const { toggleThreeD } = useThreeDStore();
|
const { toggleThreeD } = useThreeDStore();
|
||||||
|
const { selectedUser } = useSelectedUserStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="project-main">
|
<div className="project-main">
|
||||||
|
@ -81,6 +79,7 @@ const Project: React.FC = () => {
|
||||||
<RealTimeVisulization />
|
<RealTimeVisulization />
|
||||||
{activeModule !== "market" && <Tools />}
|
{activeModule !== "market" && <Tools />}
|
||||||
{isPlaying && activeModule === "simulation" && <SimulationPlayer />}
|
{isPlaying && activeModule === "simulation" && <SimulationPlayer />}
|
||||||
|
{selectedUser && <FollowPerson />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface SelectedUser {
|
||||||
|
color: string;
|
||||||
|
name: string;
|
||||||
|
location?: {
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
rotation: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectedUserStore {
|
||||||
|
selectedUser: SelectedUser | null;
|
||||||
|
setSelectedUser: (user: SelectedUser) => void;
|
||||||
|
clearSelectedUser: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
|
||||||
|
selectedUser: null,
|
||||||
|
setSelectedUser: (user) => set({ selectedUser: user }),
|
||||||
|
clearSelectedUser: () => set({ selectedUser: null }),
|
||||||
|
}));
|
|
@ -0,0 +1,22 @@
|
||||||
|
.follow-person-container{
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
outline: 8px solid var(--user-color);
|
||||||
|
outline-offset: -3px;
|
||||||
|
border-radius: 16px;
|
||||||
|
.follower-name{
|
||||||
|
background-color: var(--user-color);
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 4px 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -137,6 +137,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.user-name{
|
.user-name{
|
||||||
|
|
Loading…
Reference in New Issue