Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #25 from neynarxyz/ds/edit-frame-card-styling
Browse files Browse the repository at this point in the history
fix: edit frame card styling
  • Loading branch information
dylsteck authored Aug 17, 2024
2 parents b7b6368 + 8f9c8ff commit 493f54c
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 77 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@neynar/react",
"version": "0.8.2",
"version": "0.8.3",
"description": "Farcaster frontend component library powered by Neynar",
"main": "dist/bundle.cjs.js",
"module": "dist/bundle.es.js",
Expand Down
35 changes: 4 additions & 31 deletions src/components/atoms/icons/ExternalLinkIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,10 @@
import React from 'react';

interface ExternalLinkIconProps {
width?: number;
height?: number;
className?: string;
style?: React.CSSProperties;
}

const ExternalLinkIcon: React.FC<ExternalLinkIconProps> = ({
width = 12,
height = 12,
className = 'ml-1 text-faint',
style = {},
}) => {
const ExternalLinkIcon = () => {
return (
<svg
aria-hidden="true"
focusable="false"
role="img"
viewBox="0 0 16 16"
width={width}
height={height}
fill="currentColor"
className={className}
style={{
display: 'inline-block',
userSelect: 'none',
verticalAlign: 'text-bottom',
overflow: 'visible',
...style,
}}
>
<path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"></path>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.85855 0.555664H8.31281M8.31281 0.555664V2.73754M8.31281 0.555664L4.31445 4.11122" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.31445 1H1.31445C0.762168 1 0.314453 1.44772 0.314453 2V8C0.314453 8.55228 0.762168 9 1.31445 9H7.31445C7.86674 9 8.31445 8.55228 8.31445 8V6H7.31445V8H1.31445V2H3.31445V1Z" fill="#FFFFFF"/>
</svg>
);
};
Expand Down
7 changes: 7 additions & 0 deletions src/components/atoms/icons/LightningIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const LightningIcon = () => {
return(
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.01451 12.5187L4.96833 12.5174L4.93914 12.522C4.93113 12.516 4.92402 12.5087 4.91808 12.5005L4.51231 12.7926L4.91808 12.5005C4.9049 12.4822 4.89824 12.46 4.89913 12.4375L4.89953 12.4275V12.4176V8.81194V8.31194H4.39953H1.8876H1.88753C1.76188 8.31196 1.63853 8.27825 1.53036 8.21433C1.42218 8.1504 1.33315 8.05862 1.27256 7.94854C1.21196 7.83847 1.18203 7.71415 1.18588 7.58856C1.18973 7.46301 1.2272 7.34079 1.29438 7.23466C1.2944 7.23463 1.29442 7.23459 1.29444 7.23456L4.90001 1.54377L4.90567 1.53483L4.91095 1.52567C4.92227 1.50601 4.93988 1.49074 4.96094 1.48232C4.982 1.47389 5.00528 1.47281 5.02703 1.47924L5.02912 1.47985C5.05077 1.48614 5.06969 1.4995 5.08286 1.5178C5.09603 1.53609 5.1027 1.55827 5.1018 1.58079L5.10141 1.59073V1.60067V5.20631V5.70631H5.60141H8.11333H8.1134C8.23905 5.70629 8.3624 5.74 8.47058 5.80392C8.57875 5.86784 8.66778 5.95963 8.72838 6.06971C8.78897 6.17978 8.81891 6.3041 8.81506 6.42969C8.81121 6.55528 8.77371 6.67753 8.70649 6.78369L5.10232 12.4723C5.10219 12.4725 5.10205 12.4727 5.10191 12.4729C5.09255 12.4873 5.07969 12.499 5.06452 12.507C5.04914 12.5152 5.03191 12.5192 5.01451 12.5187Z" stroke="#FFFFFF"/>
</svg>
)
}
152 changes: 113 additions & 39 deletions src/components/molecules/FrameCard.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,152 @@
import React, { useState } from "react";
import React, { useState, useRef, useEffect } from "react";
import { styled } from "@pigment-css/react";
import ExternalLinkIcon from "../atoms/icons/ExternalLinkIcon";
import { NeynarFrame } from "../organisms/NeynarFrameCard";
import { LightningIcon } from "../atoms/icons/LightningIcon";

export type FrameCardProps = {
frames: NeynarFrame[];
frame: NeynarFrame | null;
onFrameBtnPress: (
btnIndex: number,
localFrame: NeynarFrame,
setLocalFrame: React.Dispatch<React.SetStateAction<NeynarFrame>>,
inputValue?: string
) => void;
) => Promise<void>;
};

const FrameButton = styled.button({
border: "1px solid rgba(0, 0, 0, 0.75)",
borderRadius: "12px",
padding: "4px 16px",
fontSize: "12px",
border: "1px solid rgba(255, 255, 255, 0.2)",
borderRadius: "8px",
padding: "6px 16px",
fontSize: "14px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
'@media (min-width: 768px)': {
fontSize: "16px",
},
cursor: 'pointer'
cursor: 'pointer',
backgroundColor: "#1E1E1E",
color: "white",
flex: "1 1 0",
minWidth: "0",
'&:hover': {
backgroundColor: "#2E2E2E",
}
});

const FrameContainer = styled.div({
border: "0.5px solid rgba(128, 128, 128, 0.7)",
border: "1px solid rgba(255, 255, 255, 0.1)",
margin: "8px 0",
padding: "16px",
backgroundColor: "transparent",
backgroundColor: "#121212",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "center",
alignItems: "right",
width: "100%",
maxWidth: "400px",
position: "relative",
});

const ButtonContainer = styled.div({
margin: "8px 0",
margin: "7px",
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
gap: "8px",
justifyContent: "center",
width: "97%",
});

const FrameImage = styled.img({
width: "100%",
height: "auto",
borderRadius: "8px",
borderTopLeftRadius: "8px",
borderTopRightRadius: "8px",
});

const FrameDomain = styled.div({
fontSize: "12px",
color: "#aaa",
marginTop: "4px",
textAlign: "right",
color: "#888",
marginTop: "auto",
width: "100%",
padding: "4px 0"
});

const FlexContainer = styled.div({
display: "flex",
flexDirection: "column",
gap: "4px",
width: "100%",
});

const InputField = styled.input({
border: "1px solid rgba(0, 0, 0, 0.75)",
borderRadius: "12px",
border: "1px solid rgba(255, 255, 255, 0.2)",
borderRadius: "8px",
padding: "8px",
marginTop: "8px",
width: "80%",
fontSize: "16px",
'@media (min-width: 768px)': {
fontSize: "16px",
},
width: "100%",
fontSize: "14px",
backgroundColor: "#1E1E1E",
color: "white",
});

function CastFrameBtn({ number, text, actionType, target, frameUrl, handleOnClick }: any) {
const SpinnerOverlay = styled.div({
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(0, 0, 0, 0.5)",
borderRadius: "12px",
zIndex: 10,
});

const FrameSpinnerSVG = () => {
const spinnerRef = useRef<SVGSVGElement>(null);

useEffect(() => {
if (spinnerRef.current) {
let rotation = 0;
const animate = () => {
rotation += 6;
if (spinnerRef.current) {
spinnerRef.current.style.transform = `rotate(${rotation}deg)`;
}
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
}, []);

return (
<svg
ref={spinnerRef}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="size-6 text-white"
style={{ width: '24px', height: '24px' }}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
);
};

function CastFrameBtn({ number, text, actionType, target, frameUrl, handleOnClick }: any) {
return (
<FrameButton onClick={() => handleOnClick(number)}>
{text}
{(actionType === "link" || actionType === "post_redirect" || actionType === "mint") && <ExternalLinkIcon />}
{actionType === "tx" && <LightningIcon />}
</FrameButton>
)
}

function CastFrame({ frame, onFrameBtnPress }: { frame: NeynarFrame, onFrameBtnPress: FrameCardProps['onFrameBtnPress'] }) {
const [localFrame, setLocalFrame] = useState<NeynarFrame>(frame);
const [inputValue, setInputValue] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);

const renderFrameButtons = () => {
const buttons = localFrame.buttons.map((btn) => (
Expand All @@ -101,7 +157,11 @@ function CastFrame({ frame, onFrameBtnPress }: { frame: NeynarFrame, onFrameBtnP
actionType={btn.action_type}
target={btn.target}
frameUrl={frame.frames_url}
handleOnClick={(btnIndex: number) => onFrameBtnPress(btnIndex, localFrame, setLocalFrame, inputValue)}
handleOnClick={(btnIndex: number) => {
setLoading(true);
onFrameBtnPress(btnIndex, localFrame, setLocalFrame, inputValue)
.finally(() => setLoading(false));
}}
/>
));
return <ButtonContainer>{buttons}</ButtonContainer>;
Expand All @@ -119,13 +179,27 @@ function CastFrame({ frame, onFrameBtnPress }: { frame: NeynarFrame, onFrameBtnP
}
};

const getImageStyle = () => {
switch (localFrame.image_aspect_ratio) {
case "1:1":
return { aspectRatio: "1 / 1" };
case "1.91:1":
return { aspectRatio: "1.91 / 1" };
default:
return {};
}
};

return (
<>
<FrameContainer>
{loading && (
<SpinnerOverlay><FrameSpinnerSVG /></SpinnerOverlay>
)}
{localFrame.frames_url && (
<>
<a href={localFrame.frames_url} target="_blank" rel="noopener noreferrer">
<FrameImage src={localFrame.image} alt={`Frame image for ${localFrame.frames_url}`} />
<a href={localFrame.frames_url} target="_blank" rel="noopener noreferrer" style={{ width: '100%' }}>
<FrameImage src={localFrame.image} alt={`Frame image for ${localFrame.frames_url}`} style={getImageStyle()} />
</a>
{localFrame.input?.text && (
<InputField
Expand All @@ -139,17 +213,17 @@ function CastFrame({ frame, onFrameBtnPress }: { frame: NeynarFrame, onFrameBtnP
</>
)}
</FrameContainer>
<FrameDomain>{extractDomain(localFrame.frames_url)}</FrameDomain>
{localFrame.frames_url && <FrameDomain>{extractDomain(localFrame.frames_url)}</FrameDomain>}
</>
);
}

export const FrameCard: React.FC<FrameCardProps> = ({ frames, onFrameBtnPress }) => {
export const FrameCard: React.FC<FrameCardProps> = ({ frame, onFrameBtnPress }) => {
return (
<FlexContainer>
{frames.map((frame: NeynarFrame, index: number) => (
<CastFrame key={`cast-frame-${index}`} frame={frame} onFrameBtnPress={onFrameBtnPress} />
))}
{frame ?
<CastFrame frame={frame} onFrameBtnPress={onFrameBtnPress} />
: <></>}
</FlexContainer>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/organisms/NeynarFrameCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type NeynarFrame = {
version: string;
title: string;
image: string;
image_aspect_ratio: string;
buttons: {
index: number;
title: string;
Expand Down Expand Up @@ -158,7 +159,7 @@ export const NeynarFrameCard: React.FC<NeynarFrameCardProps> = ({ url, onFrameBt

return (
<FrameCard
frames={frame ? [frame] : []}
frame={frame}
onFrameBtnPress={handleFrameBtnPress}
/>
);
Expand Down
8 changes: 3 additions & 5 deletions src/components/stories/NeynarFrameCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const TemplateWithInteractions: StoryFn<NeynarFrameCardProps> = (args) => (

export const Primary = Template.bind({});
Primary.args = {
frames: [
frame:
{
version: "vNext",
title: "Introducing Smart Wallets on Paragraph",
Expand Down Expand Up @@ -59,12 +59,11 @@ Primary.args = {
state: {},
frames_url: "https://paragraph.xyz/@blog/introducing-smart-wallets",
},
],
};

Primary.argTypes = {
hash: { table: { disable: true } },
frames: { table: { disable: true } },
frame: { table: { disable: true } },
};

export const EventsFrame = TemplateWithInteractions.bind({});
Expand All @@ -87,8 +86,7 @@ ZoraFrame.args = {
url: "https://zora.co/collect/base:0xcf6e80defd9be067f5adda2924b55c2186d3e930/5"
};


ParagraphFrame.argTypes = {
hash: { table: { disable: true } },
frames: { table: { disable: true } },
frame: { table: { disable: true } },
};

0 comments on commit 493f54c

Please sign in to comment.