Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: customize image component #505

Merged
merged 4 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/pages/guides/apps/display-frames.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,11 @@ pnpm dev
### Done! 🎉

::::

## Using Next.js Image Optimization for image proxying
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also include this in https://framesjs.org/guides/apps/display-frames-in-react-native

and also include a list of security properties here to check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React native doesn't have a built-in image proxy - what should we suggest?

Copy link
Contributor

@davidfurlong davidfurlong Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would just make a comment that it's a suggestion for privacy reasons. People can always use a next.js image proxy for this and just handle it via a next.js endpoint like /proxy?img=[src]


You can use `next/image` to proxy images to preserve privacy and prevent CORS issues.

```tsx
// [!include ~/snippets/guides/apps/react-image-optimization.tsx]
```
2 changes: 1 addition & 1 deletion docs/pages/reference/render/use-frame.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export default function Page() {
const frameState = useFrame({
// replace with your frame url
homeframeUrl:
"https://fc-polls.vercel.app/polls/73c6efda-bae7-4d46-8f36-3bb3b8377448",
"https://framesjs.org",
// corresponds to the name of the route for POST in step 3
frameActionProxy: "/frames",
// corresponds to the name of the route for GET in step 3
Expand Down
37 changes: 27 additions & 10 deletions docs/snippets/guides/apps/react-full.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "@frames.js/render/farcaster";
import { useFrame } from "@frames.js/render/use-frame";
import { fallbackFrameContext } from "@frames.js/render";
// [!code focus:43]
// [!code focus:64]
import {
FrameUI,
type FrameUIComponents,
Expand Down Expand Up @@ -33,20 +33,40 @@ const components: FrameUIComponents<StylingProps> = {};
* By default there are no styles so it is up to you to style the components as you wish.
*/
const theme: FrameUITheme<StylingProps> = {
ButtonsContainer: {
className: "flex gap-[8px] px-2 pb-2 bg-white",
},
Button: {
className:
"border text-sm text-gray-700 rounded flex-1 bg-white border-gray-300 p-2",
},
Root: {
className:
"flex flex col w-full gap-2 border rounded-lg ovrflow-hidden bg-white relative",
"flex flex-col w-full gap-2 border rounded-lg overflow-hidden bg-white relative",
},
Error: {
className:
"flex text-red-500 text-sm p-2 bg-white border border-red-500 rounded-md shadow-md aspect-square justify-center items-center",
},
LoadingScreen: {
className: "absolute top-0 left-0 right-0 bottom-0 bg-gray-300 z-10",
},
Image: {
className: "w-full object-cover max-h-full",
},
ImageContainer: {
className:
"relative w-full h-full border-b border-gray-300 overflow-hidden",
style: {
aspectRatio: "var(--frame-image-aspect-ratio)", // helps to set the fixed loading skeleton size
aspectRatio: "var(--frame-image-aspect-ratio)", // fixed loading skeleton size
},
},
TextInput: {
className: "p-[6px] border rounded border-gray-300 box-border w-full",
},
TextInputContainer: {
className: "w-full px-2",
},
};

export default function App() {
Expand All @@ -62,29 +82,26 @@ export default function App() {

const frameState = useFrame({
// replace with frame URL
homeframeUrl:
"https://fc-polls.vercel.app/polls/73c6efda-bae7-4d46-8f36-3bb3b8377448",
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner:
farcasterSigner.status === "approved" ||
farcasterSigner.status === "impersonating",
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
onSignerlessFramePress: () => {
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
logout() {
async logout() {
// here you can add your logout logic
console.log("logout");
},
Expand Down
51 changes: 51 additions & 0 deletions docs/snippets/guides/apps/react-image-optimization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type FrameUIComponents } from "@frames.js/render/ui";
import Image from "next/image";

/**
* StylingProps is a type that defines the props that can be passed to the components to style them.
*/
type StylingProps = {
className?: string;
style?: React.CSSProperties;
};

/**
* You can override components to change their internal logic or structure if you want.
* By default it is not necessary to do that since the default structure is already there
* so you can just pass an empty object and use theme to style the components.
*
* You can also style components here and completely ignore theme if you wish.
*/
const components: FrameUIComponents<StylingProps> = {
Image: (props, stylingProps) => {
if (props.status === "frame-loading") {
return <></>;
}

// Here you can add your own logic to sanitize and validate the image URL
let sanitizedSrc = props.src;

// Don't allow data URLs that are not images
stephancill marked this conversation as resolved.
Show resolved Hide resolved
if (props.src.startsWith("data:") && !props.src.startsWith("data:image")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include this in the default Image implementation @stephancill ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opened a separate PR #506

sanitizedSrc = "";
}

// Don't allow SVG data URLs
stephancill marked this conversation as resolved.
Show resolved Hide resolved
if (props.src.startsWith("data:image/svg")) {
sanitizedSrc = "";
}

return (
<Image
{...stylingProps}
src={sanitizedSrc}
onLoad={props.onImageLoadEnd}
onError={props.onImageLoadEnd}
alt="Frame image"
sizes="100vw"
height={0}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would add a todo implement comment with a quick example in comments of using the props to get the dimensions of the frame

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephancill think you missed this

width={0}
/>
);
},
};
11 changes: 4 additions & 7 deletions docs/snippets/guides/apps/react-native-full.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,29 +78,26 @@ export default function App() {

const frameState = useFrame({
// replace with frame URL
homeframeUrl:
"https://fc-polls.vercel.app/polls/73c6efda-bae7-4d46-8f36-3bb3b8377448",
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner:
farcasterSigner.status === "approved" ||
farcasterSigner.status === "impersonating",
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
onSignerlessFramePress: () => {
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
logout() {
async logout() {
// here you can add your logout logic
console.log("logout");
},
Expand Down
11 changes: 4 additions & 7 deletions docs/snippets/guides/apps/react-native-nativewind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,26 @@ export default function App() {

const frameState = useFrame({
// replace with frame URL
homeframeUrl:
"https://fc-polls.vercel.app/polls/73c6efda-bae7-4d46-8f36-3bb3b8377448",
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner:
farcasterSigner.status === "approved" ||
farcasterSigner.status === "impersonating",
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
onSignerlessFramePress: () => {
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
logout() {
async logout() {
// here you can add your logout logic
console.log("logout");
},
Expand Down
11 changes: 4 additions & 7 deletions docs/snippets/guides/apps/react-native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,26 @@ export default function App() {

const frameState = useFrame({
// replace with frame URL
homeframeUrl:
"https://fc-polls.vercel.app/polls/73c6efda-bae7-4d46-8f36-3bb3b8377448",
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner:
farcasterSigner.status === "approved" ||
farcasterSigner.status === "impersonating",
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
onSignerlessFramePress: () => {
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
logout() {
async logout() {
// here you can add your logout logic
console.log("logout");
},
Expand Down
11 changes: 4 additions & 7 deletions docs/snippets/guides/apps/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,26 @@ export default function App() {

const frameState = useFrame({
// replace with frame URL
homeframeUrl:
"https://fc-polls.vercel.app/polls/73c6efda-bae7-4d46-8f36-3bb3b8377448",
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner:
farcasterSigner.status === "approved" ||
farcasterSigner.status === "impersonating",
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
onSignerlessFramePress: () => {
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
logout() {
async logout() {
// here you can add your logout logic
console.log("logout");
},
Expand Down