Skip to content

Commit bfec4bb

Browse files
hobbescodescoopbri
andauthored
Project Socials (#142)
* feature(projects): add website and socials WIP * refactor(config): update description for updateProject * refactor(project): add functionality to update socials WIP * refactor(update_project): extract socials to child form WIP * chore: add TODO * fix(update-project): allow initial social media record to be removed * chore: remove knipignore tags * refactor(forms): extract custom UrlField * chore: remove TODO * refactor(update-project): add optimistic updates for project socials * refactor(update-project): revamp update socials usage * chore: update TODO * refactor(graphql): update order by for project socials * refactor(project-feedback): update maxH handling, update scrollbar styles, update create feedback title * chore(panda): update TODO * refactor(forms): update url field to handle empty string appropriately * refactor(create-project): remove website from form * docs: add JSDoc * refactor(url-field): update default placeholder * refactor(util): update pattern matching for getSocialMediaIcon * refactor(util): make sure getSocialMediaIcon regex matching is case insensitive * refactor(project): update render for project links * refactor(update-project): validate that project social URLs are unique * refactor(util): update getSocialMediaLabel to return apex domain for fallback value * refactor(project-links): remove unnecessary display prop * refactor: update website icon, remove global scrollbar styles * refactor(project-links): update icon sizing * refactor(project-links): update icon sizing * refactor(update-project): remove optimistic updates, update logic for handling project social updates * chore(update-project): add comment regarding dynamic created by date * refactor(update-project): update pending state for form submit button * refactor(update-project): update pending state for form submit button * chore(update-project): reorder constants and type defs * feature(update-socials): add reordering capabilities with motion * docs(components): add JSDoc to ReorderItem * refactor(layout): adjust padding of content depending on scrollbar presence to prevent CLS * refactor(project-links): update styles for menu and conditionally display tooltips * chore: update JSDoc * chore: remove unused export * refactor(url-field): camel-case -> pascal-case * chore: update JSDoc * chore: update copy * chore: update comments * chore: format * chore: `url` -> `URL` * chore: add comment transferring rationale from https://github.com/omnidotdev/backfeed-app/pull/142/files\#r2096636763 * chore: add client directive --------- Co-authored-by: Brian Cooper <[email protected]>
1 parent 850e860 commit bfec4bb

File tree

34 files changed

+1963
-261
lines changed

34 files changed

+1963
-261
lines changed

bun.lock

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dayjs": "^1.11.13",
1818
"graphql": "^16.10.0",
1919
"graphql-request": "^7.1.2",
20+
"motion": "^12.12.1",
2021
"ms": "^2.1.3",
2122
"next": "^15.3.0",
2223
"next-auth": "^5.0.0-beta.25",
@@ -1079,6 +1080,8 @@
10791080

10801081
"formdata-polyfill": ["[email protected]", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
10811082

1083+
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.12.1", "motion-utils": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-PFw4/GCREHI2suK/NlPSUxd+x6Rkp80uQsfCRFSOQNrm5pZif7eGtmG1VaD/UF1fW9tRBy5AaS77StatB3OJDg=="],
1084+
10821085
"fs-extra": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="],
10831086

10841087
"fs.realpath": ["[email protected]", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
@@ -1305,6 +1308,12 @@
13051308

13061309
"mlly": ["[email protected]", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
13071310

1311+
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-vN/3p++Ix0lVt9NH0ZqPrAy8QRTkff27t5z3z5+4BJ3cXPxtOia2EBZS4snM+JUTfl7J0JXP/5ERqu9GT/8IgA=="],
1312+
1313+
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ=="],
1314+
1315+
"motion-utils": ["[email protected]", "", {}, "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w=="],
1316+
13081317
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
13091318

13101319
"msw": ["[email protected]", "", { "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", "@bundled-es-modules/tough-cookie": "^0.1.6", "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.37.0", "@open-draft/deferred-promise": "^2.2.0", "@open-draft/until": "^2.1.0", "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "strict-event-emitter": "^0.5.1", "type-fest": "^4.26.1", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-00MyTlY3TJutBa5kiU+jWiz2z5pNJDYHn2TgPkGkh92kMmNH43RqvMXd8y/7HxNn8RjzUbvZWYZjcS36fdb6sw=="],
@@ -1899,6 +1908,8 @@
18991908

19001909
"fast-glob/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
19011910

1911+
"framer-motion/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1912+
19021913
"glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
19031914

19041915
"graphql-config/cosmiconfig": ["[email protected]", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
@@ -1933,6 +1944,8 @@
19331944

19341945
"mlly/pkg-types": ["[email protected]", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
19351946

1947+
"motion/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1948+
19361949
"no-case/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
19371950

19381951
"p-locate/p-limit": ["[email protected]", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"dayjs": "^1.11.13",
6767
"graphql": "^16.10.0",
6868
"graphql-request": "^7.1.2",
69+
"motion": "^12.12.1",
6970
"ms": "^2.1.3",
7071
"next": "^15.3.0",
7172
"next-auth": "^5.0.0-beta.25",

src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { LuSettings } from "react-icons/lu";
55

66
import { auth } from "auth";
77
import { Page } from "components/layout";
8-
import { ProjectOverview } from "components/project";
8+
import { ProjectLinks, ProjectOverview } from "components/project";
99
import {
1010
PostOrderBy,
1111
Role,
@@ -160,6 +160,9 @@ const ProjectPage = async ({ params, searchParams }: Props) => {
160160
header={{
161161
title: project.name!,
162162
description: project.description!,
163+
headerProps: {
164+
children: <ProjectLinks project={project} />,
165+
},
163166
cta: [
164167
{
165168
label: app.projectPage.header.cta.viewAllProjects.label,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client";
2+
3+
import { Icon } from "@omnidev/sigil";
4+
import { Reorder, useDragControls } from "motion/react";
5+
import { useState } from "react";
6+
import { PiDotsSixVerticalBold } from "react-icons/pi";
7+
8+
import type { ComponentProps } from "react";
9+
10+
/**
11+
* Reorder item component. Internally manages state for element's drag position.
12+
*/
13+
const ReorderItem = <T,>({
14+
children,
15+
...rest
16+
}: ComponentProps<typeof Reorder.Item<T>>) => {
17+
const [isGrabbing, setIsGrabbing] = useState(false);
18+
19+
const controls = useDragControls();
20+
21+
return (
22+
<Reorder.Item {...rest} dragListener={false} dragControls={controls}>
23+
<Icon
24+
src={PiDotsSixVerticalBold}
25+
cursor={{ _hover: isGrabbing ? "grabbing" : "grab" }}
26+
touchAction="none"
27+
onPointerUp={() => setIsGrabbing(false)}
28+
// @ts-ignore: TODO fix implicit any type upstream in Sigil
29+
onPointerDown={(evt) => {
30+
evt.preventDefault();
31+
controls.start(evt);
32+
setIsGrabbing(true);
33+
}}
34+
/>
35+
36+
{children}
37+
</Reorder.Item>
38+
);
39+
};
40+
41+
export default ReorderItem;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client";
2+
3+
import { Icon } from "@omnidev/sigil";
4+
5+
import { getSocialMediaIcon } from "lib/util";
6+
7+
import type { IconProps } from "@omnidev/sigil";
8+
9+
interface Props extends Omit<IconProps, "src"> {
10+
/** URL to dynamically determine icon source. */
11+
url: string;
12+
}
13+
14+
/**
15+
* Social media icon, dynamically determined based on the provided URL.
16+
*/
17+
const SocialMediaIcon = ({ url, ...rest }: Props) => (
18+
<Icon src={getSocialMediaIcon(url)} {...rest} />
19+
);
20+
21+
export default SocialMediaIcon;

src/components/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export { default as Image } from "./Image/Image";
1717
export { default as Link } from "./Link/Link";
1818
export { default as LogoLink } from "./LogoLink/LogoLink";
1919
export { default as OverflowText } from "./OverflowText/OverflowText";
20+
export { default as ReorderItem } from "./ReorderItem/ReorderItem";
2021
export { default as SkeletonArray } from "./SkeletonArray/SkeletonArray";
22+
export { default as SocialMediaIcon } from "./SocialMediaIcon/SocialMediaIcon";
2123
export { default as Spinner } from "./Spinner/Spinner";
2224
export { default as StatusBadge } from "./StatusBadge/StatusBadge";
2325
export { default as Tooltip } from "./Tooltip/Tooltip";
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Button, HStack, Icon, Input, Label, Text } from "@omnidev/sigil";
2+
import { FiX } from "react-icons/fi";
3+
4+
import { Field } from "components/form";
5+
import { token } from "generated/panda/tokens";
6+
import { useFieldContext } from "lib/hooks";
7+
8+
import type { ButtonProps, InputProps, StackProps } from "@omnidev/sigil";
9+
import type { FormFieldErrorProps } from "components/form";
10+
import type { IconType } from "react-icons";
11+
12+
interface Props extends InputProps {
13+
/** URL icon. */
14+
icon: IconType;
15+
/** Label for the input field. */
16+
label?: string;
17+
/** Field container props. */
18+
containerProps?: StackProps;
19+
/** Additional props for the error component. */
20+
errorProps?: Partial<FormFieldErrorProps>;
21+
/** Whether to display the remove field trigger. */
22+
displayRemoveTrigger?: boolean;
23+
/** Additional props to be passed to the remove field trigger. */
24+
removeFieldProps?: ButtonProps;
25+
}
26+
27+
/**
28+
* URL field component for form inputs.
29+
*/
30+
const URLField = ({
31+
icon,
32+
label,
33+
containerProps,
34+
errorProps,
35+
displayRemoveTrigger = true,
36+
removeFieldProps,
37+
...props
38+
}: Props) => {
39+
const { handleChange, state, name } = useFieldContext<string>();
40+
41+
return (
42+
<Field errorProps={errorProps} {...containerProps}>
43+
{label && <Label htmlFor={name}>{label}</Label>}
44+
45+
<HStack>
46+
<Icon src={icon} />
47+
48+
<HStack
49+
gap={0}
50+
flex={1}
51+
overflow="hidden"
52+
borderWidth="1px"
53+
borderRadius="sm"
54+
borderColor="border.subtle"
55+
transitionDuration="normal"
56+
transitionProperty="box-shadow, border-color"
57+
transitionTimingFunction="default"
58+
_focusWithin={{
59+
borderColor: "accent.default",
60+
boxShadow: `0 0 0 1px ${token("colors.accent.default")}`,
61+
}}
62+
>
63+
<Text p={2} bgColor="background.subtle">
64+
https://
65+
</Text>
66+
67+
<Input
68+
id={name}
69+
placeholder="github.com/..."
70+
value={state.value.replace(/^(https:\/\/|http:\/\/)/i, "")}
71+
onChange={(evt) => {
72+
const updatedValue = evt.target.value.replace(
73+
/^(https:\/\/|http:\/\/)/i,
74+
"",
75+
);
76+
77+
updatedValue.length
78+
? handleChange(`https://${updatedValue}`)
79+
: handleChange("");
80+
}}
81+
borderLeftRadius={0}
82+
borderWidth={0}
83+
_focus={{
84+
boxShadow: "none",
85+
}}
86+
{...props}
87+
/>
88+
</HStack>
89+
90+
{displayRemoveTrigger && (
91+
<Button
92+
variant="icon"
93+
bgColor="transparent"
94+
color={{
95+
base: "foreground.subtle",
96+
_hover: {
97+
base: "omni.ruby",
98+
_disabled: "foreground.subtle",
99+
},
100+
}}
101+
opacity={{ _disabled: 0.8 }}
102+
{...removeFieldProps}
103+
>
104+
<Icon src={FiX} />
105+
</Button>
106+
)}
107+
</HStack>
108+
</Field>
109+
);
110+
};
111+
112+
export default URLField;

src/components/form/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { default as InputField } from "./InputField/InputField";
77
export { default as SingularComboboxField } from "./SingularComboboxField/SingularComboboxField";
88
export { default as SubmitForm } from "./SubmitForm/SubmitForm";
99
export { default as TextareaField } from "./TextareaField/TextareaField";
10+
export { default as URLField } from "./URLField/URLField";

src/components/layout/Layout/Layout.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ const Layout = ({ children }: PropsWithChildren) => (
3030
<Header />
3131
</Flex>
3232

33-
<Flex direction="column" position="relative" w="100%" h="100dvh" gap={0}>
33+
<Flex
34+
direction="column"
35+
position="relative"
36+
w="100%"
37+
h="100dvh"
38+
gap={0}
39+
// ! NB: This helps prevent CLS on pages when the content size is dynamic, and therefore the scrollbar may or may not be visible. See: https://stackoverflow.com/a/30293718
40+
paddingLeft="calc(100vw - 100%)"
41+
>
3442
{/* TODO fix styles not appropriately being applied (https://linear.app/omnidev/issue/OMNI-109/look-into-panda-css-styling-issues) */}
3543
<sigil.main w="full" flex={1} css={css.raw({ mt: "header" })}>
3644
{children}

src/components/layout/Page/Page.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { Flex, Stack, Text } from "@omnidev/sigil";
3+
import { Flex, HStack, Stack, Text } from "@omnidev/sigil";
44

55
import { Breadcrumb, CallToAction } from "components/core";
66

@@ -49,9 +49,18 @@ const Page = ({ breadcrumbs, header, children, ...rest }: Props) => (
4949
gap={4}
5050
>
5151
<Stack>
52-
<Text as="h1" fontSize="3xl" fontWeight="semibold" lineHeight={1.3}>
53-
{header.title}
54-
</Text>
52+
<HStack gap={1}>
53+
<Text
54+
as="h1"
55+
fontSize="3xl"
56+
fontWeight="semibold"
57+
lineHeight={1.3}
58+
>
59+
{header.title}
60+
</Text>
61+
62+
{header.headerProps?.children}
63+
</HStack>
5564

5665
{header.description && (
5766
<Text
@@ -69,6 +78,7 @@ const Page = ({ breadcrumbs, header, children, ...rest }: Props) => (
6978
gap={4}
7079
width={{ base: "full", md: "auto" }}
7180
direction={{ base: "column", sm: "row" }}
81+
placeSelf="flex-start"
7282
>
7383
{header.cta?.map((action) => (
7484
<CallToAction key={action.label} action={action} />

0 commit comments

Comments
 (0)