Skip to content

Commit f10c7c0

Browse files
authored
Merge branch 'master' into dependabot/npm_and_yarn/types/node-18.16.3
2 parents e091c2d + 66435d5 commit f10c7c0

File tree

9 files changed

+1010
-193
lines changed

9 files changed

+1010
-193
lines changed

client/src/components/lines/BadgeItem.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,54 @@
1+
import { useCallback, useState } from "react";
12
import { Badge } from "../../types";
3+
import { cn } from "../ui/utils";
4+
import { useMultistepContext } from "../../hooks/useMultistepContext";
25

36
type Props = {
47
badge: Badge;
8+
lineNumber: number;
59
removeBadge: (position: number) => void;
610
};
711

8-
const BadgeItem = ({ badge, removeBadge }: Props) => {
12+
const BadgeItem = ({ badge, removeBadge, lineNumber }: Props) => {
13+
const [dragged, setDragged] = useState(false);
14+
const { setGrabbedBadge, grabbedBadge } = useMultistepContext();
15+
16+
const handleDragStart = useCallback(
17+
(e: React.DragEvent<HTMLButtonElement>) => {
18+
// draw the grabbed div again so it's not hidden
19+
const { left, top } = e.currentTarget.getBoundingClientRect();
20+
e.dataTransfer.setDragImage(
21+
e.currentTarget,
22+
e.clientX - left,
23+
e.clientY - top
24+
);
25+
26+
setGrabbedBadge({
27+
badge,
28+
lineNumber,
29+
badgeWidth: e.currentTarget.clientWidth,
30+
});
31+
32+
setTimeout(() => setDragged(true), 0);
33+
},
34+
[lineNumber, badge]
35+
);
36+
937
return (
1038
<button
1139
draggable
12-
className="flex cursor-grab select-none items-center gap-3 bg-gh-bg-secondary px-3 py-[.45rem] transition-all duration-150 hover:bg-gh-gray"
40+
onDragStart={handleDragStart}
41+
onDragEnd={() => {
42+
setDragged(false);
43+
setGrabbedBadge(undefined);
44+
}}
45+
className={cn(
46+
"flex cursor-grab select-none items-center gap-3 border border-gh-bg px-3 py-[.45rem] transition-all duration-150",
47+
dragged
48+
? "border-gh-bg-secondary bg-gh-bg-dark text-gh-bg-dark"
49+
: "bg-gh-bg-secondary text-white",
50+
grabbedBadge === undefined ? "hover:bg-gh-gray" : ""
51+
)}
1352
onClick={(e) => {
1453
// double click
1554
if (e.detail === 2) {
@@ -19,7 +58,7 @@ const BadgeItem = ({ badge, removeBadge }: Props) => {
1958
>
2059
<img
2160
onError={(e) => (e.currentTarget.style.display = "none")}
22-
className="h-4 w-4 select-none"
61+
className={cn("h-4 w-4 select-none", dragged ? "opacity-0" : "")}
2362
alt={`${badge.icon}`}
2463
// the image is in dataUrl format if starts with data:image, else display it from simpleicons
2564
src={
@@ -32,7 +71,7 @@ const BadgeItem = ({ badge, removeBadge }: Props) => {
3271
/>
3372

3473
{badge.label.trim().length > 0 && (
35-
<span className="py-[.145rem] font-dejavu text-[.70rem] font-bold uppercase leading-none tracking-wider text-white">
74+
<span className="py-[.145rem] font-dejavu text-[.70rem] font-bold uppercase leading-none tracking-wider">
3675
{badge.label}
3776
</span>
3877
)}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useCallback, useState } from "react";
2+
import { useMultistepContext } from "../../hooks/useMultistepContext";
3+
import { cn } from "../ui/utils";
4+
5+
type Props = {
6+
lineNumber: number;
7+
position: number;
8+
};
9+
10+
const BadgePlaceholder = ({ lineNumber, position }: Props) => {
11+
const { grabbedBadge, setGrabbedBadge, insertBadge } = useMultistepContext();
12+
const [hovered, setHovered] = useState(false);
13+
14+
const shouldDropBeAllowed = useCallback((): boolean => {
15+
if (!grabbedBadge) return false;
16+
if (grabbedBadge.lineNumber !== lineNumber) return false;
17+
18+
// if the placeholder is next to the badge
19+
const { badge } = grabbedBadge;
20+
if (position === badge.position || position === badge.position + 1) {
21+
return false;
22+
}
23+
24+
return true;
25+
}, [grabbedBadge, lineNumber, position]);
26+
27+
const handleDragOver = useCallback(
28+
(e: React.DragEvent<HTMLDivElement>) => {
29+
if (!shouldDropBeAllowed()) return;
30+
31+
e.preventDefault();
32+
},
33+
[shouldDropBeAllowed]
34+
);
35+
36+
const handleDrop = useCallback(
37+
(e: React.DragEvent<HTMLDivElement>) => {
38+
if (!grabbedBadge) return;
39+
e.preventDefault();
40+
41+
insertBadge(lineNumber, position, grabbedBadge);
42+
setHovered(false);
43+
setGrabbedBadge(undefined);
44+
},
45+
[grabbedBadge, insertBadge, lineNumber, position]
46+
);
47+
48+
return (
49+
<div
50+
onDragOver={handleDragOver}
51+
onDrop={handleDrop}
52+
onDragLeave={() => setHovered(false)}
53+
onDragEnter={() => (shouldDropBeAllowed() ? setHovered(true) : {})}
54+
className={cn(
55+
"h-[31.73px] border-r-[.5rem] border-gh-bg",
56+
hovered ? "border-l-[.5rem] bg-gh-bg-dark" : "bg-transparent"
57+
)}
58+
style={{
59+
width:
60+
hovered && grabbedBadge
61+
? `calc(${grabbedBadge.badgeWidth}px + 1rem)`
62+
: "0",
63+
}}
64+
/>
65+
);
66+
};
67+
68+
export default BadgePlaceholder;

client/src/components/lines/LineItem.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useMultistepContext } from "../../hooks/useMultistepContext";
22
import { Line } from "../../types";
3-
import Hr from "../ui/Hr";
43
import { formatNumberWord } from "../ui/utils";
54
import BadgeItem from "./BadgeItem";
5+
import BadgePlaceholder from "./BadgePlaceholder";
66
import NewBadge from "./NewBadge";
7+
import React from "react";
78

89
type Props = {
910
line: Line;
@@ -13,34 +14,43 @@ const LineItem = ({ line }: Props) => {
1314
const { addBadge, removeBadge } = useMultistepContext();
1415

1516
return (
16-
<div className="w-full rounded-md border border-gh-border bg-gh-bg">
17-
<div className="rounded-tl-md rounded-tr-md border-b border-gh-border bg-gh-bg-secondary px-3 py-2 leading-none text-gh-text">
17+
<article className="w-full rounded-md border border-gh-border bg-gh-bg">
18+
<h3 className="rounded-tl-md rounded-tr-md border-b border-gh-border bg-gh-bg-secondary px-3 py-2 leading-none text-gh-text">
1819
{formatNumberWord(line.lineNumber)} line
19-
</div>
20+
</h3>
2021

21-
<div className="flex flex-col gap-2 px-3 py-2">
22+
<div className="flex flex-col gap-2 px-1 py-2">
2223
{line.badges.length < 1 ? (
2324
<div className="text-center text-sm text-gh-text-secondary">
2425
🥱 No badges selected.
2526
</div>
2627
) : (
27-
<div className="flex flex-wrap gap-2">
28+
<div className="flex flex-wrap">
29+
<BadgePlaceholder lineNumber={line.lineNumber} position={0} />
30+
2831
{[...line.badges]
2932
.sort((a, z) => a.position - z.position)
3033
.map((badge, i) => (
31-
<BadgeItem
32-
key={i}
33-
badge={badge}
34-
removeBadge={(p) => removeBadge(line.lineNumber, p)}
35-
/>
34+
<React.Fragment key={i}>
35+
<BadgeItem
36+
lineNumber={line.lineNumber}
37+
badge={badge}
38+
removeBadge={(p) => removeBadge(line.lineNumber, p)}
39+
/>
40+
<BadgePlaceholder
41+
lineNumber={line.lineNumber}
42+
position={i + 1}
43+
/>
44+
</React.Fragment>
3645
))}
3746
</div>
3847
)}
3948

40-
<Hr />
41-
<NewBadge addBadge={(badge) => addBadge(line.lineNumber, badge)} />
49+
<div className="mx-2 border-t border-gh-border pt-2">
50+
<NewBadge addBadge={(badge) => addBadge(line.lineNumber, badge)} />
51+
</div>
4252
</div>
43-
</div>
53+
</article>
4454
);
4555
};
4656

client/src/components/ui/RepositoryLink.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ const RepositoryLink = ({ user, repository, isPublic }: Props) => {
1818
description: "This is a description",
1919
});
2020

21-
// useEffect(() => {
22-
// axios
23-
// .get<GithubResponse>(`https://api.github.com/repos/${user}/${repository}`)
24-
// .then((res) => setGithubStats(res.data));
25-
// }, []);
21+
useEffect(() => {
22+
axios
23+
.get<GithubResponse>(`https://api.github.com/repos/${user}/${repository}`)
24+
.then((res) => setGithubStats(res.data));
25+
}, []);
2626

2727
return (
2828
<div>

client/src/context/MultistepContext.tsx

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useMultistepForm } from "../hooks/useMultistepForm";
33
import PageOne from "../components/form/PageOne";
44
import PageTwo from "../components/form/PageTwo";
55
import PageThree from "../components/form/PageThree";
6-
import { Badge, Card } from "../types";
6+
import { Badge, BadgeDataTransfer, Card } from "../types";
77
import PageFour from "../components/form/PageFour";
88
import PageFive from "../components/form/PageFive";
99
import PageSix from "../components/form/PageSix";
@@ -21,6 +21,15 @@ export interface MultistepContextType {
2121
setCard: React.Dispatch<React.SetStateAction<Card>>;
2222
addBadge: (lineNumber: number, badge: Omit<Badge, "position">) => void;
2323
removeBadge: (lineNumber: number, position: number) => void;
24+
grabbedBadge: BadgeDataTransfer | undefined;
25+
setGrabbedBadge: React.Dispatch<
26+
React.SetStateAction<BadgeDataTransfer | undefined>
27+
>;
28+
insertBadge: (
29+
lineNumber: number,
30+
position: number,
31+
bdt: BadgeDataTransfer
32+
) => void;
2433
}
2534

2635
export const MultistepContext = createContext<MultistepContextType>(
@@ -32,6 +41,8 @@ interface MultistepProviderProps {
3241
}
3342

3443
export const MultistepProvider: FC<MultistepProviderProps> = ({ children }) => {
44+
const [grabbedBadge, setGrabbedBadge] = useState<BadgeDataTransfer>();
45+
3546
const [card, setCard] = useState<Card>({
3647
title: "My Tech Stack",
3748
theme: "github",
@@ -50,15 +61,9 @@ export const MultistepProvider: FC<MultistepProviderProps> = ({ children }) => {
5061
{
5162
lineNumber: 1,
5263
badges: [
53-
{ position: 1, color: "auto", icon: "react", label: "react" },
5464
{ position: 0, color: "auto", icon: "laravel", label: "laravel" },
55-
],
56-
},
57-
{
58-
lineNumber: 2,
59-
badges: [
60-
{ position: 1, color: "auto", icon: "spring", label: "spring" },
61-
{ position: 0, color: "auto", icon: "c", label: "c" },
65+
{ position: 1, color: "auto", icon: "react", label: "react" },
66+
{ position: 2, color: "auto", icon: "c", label: "c" },
6267
],
6368
},
6469
],
@@ -71,53 +76,92 @@ export const MultistepProvider: FC<MultistepProviderProps> = ({ children }) => {
7176

7277
const updateCard = useCallback(
7378
() => (updated: Partial<Card>) => {
74-
setCard((prev) => ({ ...prev, ...updated }));
79+
setCard((prev) => ({ ...prev, updated }));
7580
},
7681
[]
7782
);
7883

7984
const addBadge = useCallback(
8085
(lineNumber: number, badge: Omit<Badge, "position">) => {
8186
setCard((prev) => {
82-
const newObj = { ...prev };
83-
const lineIdx = newObj.lines.findIndex(
87+
const newCard = structuredClone(prev);
88+
const lineIdx = newCard.lines.findIndex(
8489
(x) => x.lineNumber === lineNumber
8590
);
8691

8792
// line with the specified lineNumber doesn't exist
8893
if (lineIdx === -1) return prev;
8994

9095
// update the badges
91-
newObj.lines[lineIdx].badges = [
92-
...newObj.lines[lineIdx].badges,
93-
{ ...badge, position: newObj.lines[lineIdx].badges.length },
96+
newCard.lines[lineIdx].badges = [
97+
...newCard.lines[lineIdx].badges,
98+
{ ...badge, position: newCard.lines[lineIdx].badges.length },
9499
];
95100

96-
return newObj;
101+
return newCard;
97102
});
98103
},
99104
[]
100105
);
101106

102107
const removeBadge = useCallback((lineNumber: number, position: number) => {
103108
setCard((prev) => {
104-
const newObj = { ...prev };
105-
const lineIdx = newObj.lines.findIndex(
109+
const newCard = structuredClone(prev);
110+
const lineIdx = newCard.lines.findIndex(
106111
(x) => x.lineNumber === lineNumber
107112
);
108113

109114
// line with the specified lineNumber doesn't exist
110115
if (lineIdx === -1) return prev;
111116

112-
// remove the badge
113-
newObj.lines[lineIdx].badges = newObj.lines[lineIdx].badges.filter(
114-
(x) => x.position !== position
115-
);
117+
// remove the badge and rearrange the positions
118+
newCard.lines[lineIdx].badges = newCard.lines[lineIdx].badges
119+
.sort((a, z) => a.position - z.position)
120+
.filter((x) => x.position !== position)
121+
.map((prev, i) => ({ ...prev, position: i }));
116122

117-
return newObj;
123+
return newCard;
118124
});
119125
}, []);
120126

127+
const insertBadge = useCallback(
128+
(lineNumber: number, position: number, bdt: BadgeDataTransfer) => {
129+
// If the grabbed badge is in the left side of the new position,
130+
// we need to decrement the position by one,
131+
// because we grabbed (technically removed) one from the new position's left side.
132+
if (position > bdt.badge.position) position--;
133+
134+
setCard((prev) => {
135+
const newCard = structuredClone(prev);
136+
const lineIdx = newCard.lines.findIndex(
137+
(x) => x.lineNumber === lineNumber
138+
);
139+
140+
// line with the specified lineNumber doesn't exist
141+
if (lineIdx === -1) return prev;
142+
143+
// remove the old badge
144+
newCard.lines[lineIdx].badges = newCard.lines[lineIdx].badges
145+
.sort((a, z) => a.position - z.position)
146+
.filter((x) => x.position !== bdt.badge.position);
147+
148+
// insert the new badge
149+
newCard.lines[lineIdx].badges.splice(position, 0, bdt.badge);
150+
151+
// rearrange the positions
152+
newCard.lines[lineIdx].badges = newCard.lines[lineIdx].badges.map(
153+
(prev, i) => ({
154+
...prev,
155+
position: i,
156+
})
157+
);
158+
159+
return newCard;
160+
});
161+
},
162+
[]
163+
);
164+
121165
const {
122166
currentPage,
123167
currentPageIndex,
@@ -150,6 +194,9 @@ export const MultistepProvider: FC<MultistepProviderProps> = ({ children }) => {
150194
setCard,
151195
addBadge,
152196
removeBadge,
197+
grabbedBadge,
198+
setGrabbedBadge,
199+
insertBadge,
153200
}}
154201
>
155202
{children}

0 commit comments

Comments
 (0)