···11-import { useState } from "react";
22-import {
33- BlockCardSmall,
44- CloseContrastSmall,
55- CloseTiny,
66- BlockImageSmall,
77- LinkSmall,
88- BlockLinkSmall,
99-} from "../../components/Icons";
1010-import { theme } from "../../tailwind.config";
1111-import useMeasure from "react-use-measure";
1212-1313-export const TextBlock = (props: { defaultValue: string; lines: number }) => {
1414- let [value, setValue] = useState(props.defaultValue);
1515- let [focus, setFocus] = useState(false);
1616-1717- return (
1818- <div className="textBlockWrapper relative group/text pb-3">
1919- <textarea
2020- className={`textBlock w-full p-0 border-none outline-none resize-none align-top bg-transparent`}
2121- style={{
2222- height: `${props.lines * 24}px`,
2323- }}
2424- defaultValue={props.defaultValue}
2525- onChange={(e) => setValue(e.target.value)}
2626- onFocus={() => setFocus(true)}
2727- onBlur={() => setFocus(false)}
2828- />
2929- <div className="blockTypeSelectorWrapper absolute top-0 right-0">
3030- <BlockTypeSelector focus={focus} empty={value === ""} />
3131- </div>
3232- </div>
3333- //add button to select block type when empty
3434- );
3535-};
3636-3737-const BlockTypeSelector = (props: { focus: boolean; empty: boolean }) => {
3838- return (
3939- <div
4040- className={`blockTypeSelector
4141- ${props.focus && props.empty ? "block" : "hidden"}
4242- ${props.empty && "group-hover/text:block"}`}
4343- >
4444- <div className="flex gap-1 ">
4545- <button className="text-tertiary hover:text-accent">
4646- <BlockImageSmall />
4747- </button>
4848-4949- <button className="text-tertiary hover:text-accent">
5050- <BlockCardSmall />
5151- </button>
5252-5353- <button className="text-tertiary hover:text-accent">
5454- <BlockLinkSmall />
5555- </button>
5656- </div>
5757- </div>
5858- );
5959-};
6060-export const ImageBlock = (props: { src: string; cardHeight: number }) => {
6161- return (
6262- <div className="pb-4 pt-2 relative group/image flex w-fit place-self-center justify-center">
6363- <button className="absolute right-2 top-6 z-10 hidden group-hover/image:block">
6464- pt-2 pb-2 px-2 grow min-w-0{" "}
6565- <CloseContrastSmall
6666- fill={theme.colors.primary}
6767- stroke={theme.colors["bg-card"]}
6868- />{" "}
6969- </button>
7070- <img
7171- src={props.src}
7272- alt="image"
7373- className={`w-max relative`}
7474- style={{ maxHeight: `calc(${props.cardHeight}px - 48px)` }}
7575- />
7676- </div>
7777- );
7878-};
7979-8080-const ImageRemoveButton = () => {
8181- return (
8282- <svg
8383- width="24"
8484- height="24"
8585- viewBox="0 0 24 24"
8686- fill="none"
8787- xmlns="http://www.w3.org/2000/svg"
8888- >
8989- <path
9090- fillRule="evenodd"
9191- clipRule="evenodd"
9292- d="M19.5314 17.2686C20.1562 17.8935 20.1562 18.9065 19.5314 19.5314C18.9065 20.1562 17.8935 20.1562 17.2686 19.5314L12 14.2627L6.73137 19.5314C6.10653 20.1562 5.09347 20.1562 4.46863 19.5314C3.84379 18.9065 3.84379 17.8935 4.46863 17.2686L9.73726 12L4.46863 6.73137C3.84379 6.10653 3.84379 5.09347 4.46863 4.46863C5.09347 3.84379 6.10653 3.84379 6.73137 4.46863L12 9.73726L17.2686 4.46863C17.8935 3.84379 18.9065 3.84379 19.5314 4.46863C20.1562 5.09347 20.1562 6.10653 19.5314 6.73137L14.2627 12L19.5314 17.2686Z"
9393- fill={theme.colors.primary}
9494- />
9595- <path
9696- fillRule="evenodd"
9797- clipRule="evenodd"
9898- d="M17.2686 4.46863C17.8935 3.84379 18.9065 3.84379 19.5314 4.46863C20.1562 5.09347 20.1562 6.10653 19.5314 6.73137L14.2627 12L19.5314 17.2686C20.1562 17.8935 20.1562 18.9065 19.5314 19.5314C18.9065 20.1562 17.8935 20.1562 17.2686 19.5314L12 14.2627L6.73137 19.5314C6.10653 20.1562 5.09347 20.1562 4.46863 19.5314C3.84379 18.9065 3.84379 17.8935 4.46863 17.2686L9.73726 12L4.46863 6.73137C3.84379 6.10653 3.84379 5.09347 4.46863 4.46863C5.09347 3.84379 6.10653 3.84379 6.73137 4.46863L12 9.73726L17.2686 4.46863ZM12 7.61594L16.208 3.40797C17.4186 2.19734 19.3814 2.19734 20.592 3.40797C21.8027 4.61859 21.8027 6.58141 20.592 7.79203L16.3841 12L20.592 16.208C21.8027 17.4186 21.8027 19.3814 20.592 20.592C19.3814 21.8027 17.4186 21.8027 16.208 20.592L12 16.3841L7.79203 20.592C6.58141 21.8027 4.61859 21.8027 3.40797 20.592C2.19734 19.3814 2.19734 17.4186 3.40797 16.208L7.61594 12L3.40797 7.79203C2.19734 6.5814 2.19734 4.6186 3.40797 3.40797C4.61859 2.19734 6.58141 2.19734 7.79203 3.40797L12 7.61594Z"
9999- fill={theme.colors["bg-card"]}
100100- />
101101- </svg>
102102- );
103103-};
104104-105105-export const CardBlock = (props: {
106106- title?: string;
107107- body?: string;
108108- screenshot?: string;
109109- cardHeight?: number;
110110-}) => {
111111- let [blockRef, { width: blockWidth }] = useMeasure();
112112-113113- return (
114114- <div
115115- ref={blockRef}
116116- className="cardBlockWrapper relative group w-full h-[104px] mb-3 border border-border outline outline-1 outline-transparent hover:outline-border rounded-lg flex overflow-hidden"
117117- >
118118- {/*
119119- all headers are reduced to h3 styling to keep the card block consistent and legible
120120- the card block will render as much text as can fit in the block without overflowing
121121- if the text overflows, it will be truncated and an ellipsis will be added to the end of the text
122122-123123- what happens in there is no text content at all?
124124- what happens if the title spans two lines, how much space is left for the body?
125125-126126- I think the logic i used is flawed and won't stand up to reality so let's figure this out in earnest later
127127- */}
128128-129129- {/* TODO: implement long press to select and bring up options (such as remove)*/}
130130- <div className="absolute top-0.5 right-0.5 group-hover:block hidden text-border hover:text-accent ">
131131- <CloseTiny />
132132- </div>
133133-134134- <div className="cardBlockContent grow p-2">
135135- {props.title && (
136136- <div className={`w-full grow text-base font-bold line-clamp-3`}>
137137- {props.title}
138138- </div>
139139- )}
140140- {props.body && (
141141- <div
142142- className={`w-full grow text-sm ${!props.title ? "line-clamp-4" : "line-clamp-3"}`}
143143- >
144144- {props.body}
145145- </div>
146146- )}
147147- </div>
148148-149149- <div
150150- className={`cardBlockPreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border-light `}
151151- style={{ backgroundImage: `url(${props.screenshot})` }}
152152- />
153153- </div>
154154- );
155155-};
156156-157157-export const ExternalLinkBlock = () => {
158158- let [title, setTitle] = useState("Title");
159159- let [description, setDescription] = useState(
160160- "hello, this is a little description. I want it to be a little bit long so that I can see if it wrapping around but it thought it was long enought and it wasn't actually so im adding a little more on",
161161- );
162162-163163- return (
164164- <a
165165- href="www.google.com"
166166- className="externalLinkBlock relative group h-[104px] mb-3 flex border border-border hover:border-accent outline outline-1 outline-transparent hover:outline-accent rounded-lg overflow-hidden"
167167- >
168168- <div className="absolute top-0.5 right-0.5 group-hover:block hidden text-accent">
169169- <CloseTiny />
170170- </div>
171171- <div className="pt-2 pb-2 px-2 grow min-w-0">
172172- <div className="flex flex-col w-full min-w-0 h-full grow ">
173173- <textarea
174174- // when this textarea is replaced a responsive one,
175175- // make it such that the text area is only as wide as it's contents
176176- // such that click anything but the literaly words of the title and description will nav you to the link
177177- // and clicking the title or description will allow you to edit them
178178- className={`linkBlockTitle bg-transparent -mb-0.5 border-none text-base font-bold outline-none resize-none align-top border h-[24px] line-clamp-1`}
179179- defaultValue={title}
180180- onClick={(e) => {
181181- e.preventDefault();
182182- e.stopPropagation();
183183- }}
184184- onChange={(e) => {
185185- setTitle(e.target.value);
186186- }}
187187- />
188188-189189- <textarea
190190- className={`linkBlockDescription text-sm bg-transparent border-none outline-none resize-none align-top grow line-clamp-2`}
191191- defaultValue={description}
192192- onClick={(e) => {
193193- e.preventDefault();
194194- e.stopPropagation();
195195- }}
196196- onChange={(e) => {
197197- setDescription(e.target.value);
198198- }}
199199- />
200200- <div className="inline-block place-self-end w-full text-xs text-tertiary italic line-clamp-1 truncate group-hover:text-accent">
201201- https://www.flickr.com/photos/biodivlibrary/https://www.flickr.com/photos/biodivlibrary/https://www.flickr.com/photos/biodivlibrary/
202202- </div>
203203- </div>
204204- </div>
205205-206206- <div
207207- className={`linkBlockPreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border group-hover:border-accent`}
208208- style={{ backgroundImage: `url(./test-link.png)` }}
209209- />
210210- </a>
211211- );
212212-};
-198
app/test/Card.tsx
···11-import React from "react";
22-import { ButtonPrimary } from "../../components/Buttons";
33-import { TextBlock, ImageBlock, CardBlock, ExternalLinkBlock } from "./Blocks";
44-import { useIsMobile, useIsInitialRender } from "src/hooks/isMobile";
55-import { TextToolbar } from "./TextToolbar";
66-77-export const Card = (props: {
88- first?: boolean;
99- focused?: boolean;
1010- id: string;
1111- index: number;
1212- setFocusedCardIndex: (index: number) => void;
1313- setCards: ([]) => void;
1414- cards: number[];
1515- card: number;
1616- cardWidth: number;
1717- cardHeight: number;
1818-}) => {
1919- let isMobile = useIsMobile();
2020- let isInitialRender = useIsInitialRender();
2121-2222- if (isInitialRender) return null;
2323- return (
2424- <>
2525- {/* if the card is the first one in the list, remove this div... can we do with :before? */}
2626- {!props.first && <div className="w-6 md:snap-center" />}
2727- <div
2828- id={props.id}
2929- className={`
3030- cardWrapper w-[calc(100vw-12px)] md:w-[calc(50vw-32px)] max-w-prose
3131- relative
3232- grow flex flex-col
3333- overflow-y-scroll no-scrollbar
3434- snap-center
3535- rounded-lg border
3636- ${props.focused ? "shadow-md border-border" : "border-border-light"}
3737-3838- `}
3939- style={{
4040- backgroundColor: "rgba(var(--bg-card), var(--bg-card-alpha))",
4141- }}
4242- >
4343- <CardContent cardHeight={props.cardHeight} />
4444-4545- {!isMobile && props.focused ? (
4646- <div className="textToolbarWrapper sticky bottom-3 w-fit flex gap-[6px] items-center py-2 px-3 mx-auto bg-bg-card border border-border rounded-full shadow-md">
4747- <TextToolbar />
4848- </div>
4949- ) : null}
5050- </div>
5151- </>
5252- );
5353-};
5454-5555-const CardContent = (props: { cardHeight: number }) => {
5656- return (
5757- <div className=" p-3 sm:p-4 flex flex-col">
5858- <h2>Chapter 1</h2>
5959- <TextBlock
6060- lines={6}
6161- defaultValue="It is a truth universally acknowledged, that a single man in possession of a good fortune must be in want of a wife. However little known the feelings or views of such a man may be on his first entering a neighbourhood, this truth is so well fixed in the minds of the surrounding families, that he is considered as the rightful property of some one or other of their daughters."
6262- />
6363- <TextBlock
6464- lines={2}
6565- defaultValue="“My dear Mr. Bennet,” said his lady to him one day, “have you heard that Netherfield Park is let at last?”"
6666- />
6767- <TextBlock lines={1} defaultValue="Mr. Bennet replied that he had not." />
6868- <TextBlock
6969- lines={2}
7070- defaultValue="“But it is,” returned she; “for Mrs. Long has just been here, and she told me all about it.”"
7171- />
7272- <TextBlock lines={1} defaultValue="Mr. Bennet made no answer." />
7373- <TextBlock
7474- lines={2}
7575- defaultValue="“Do not you want to know who has taken it?” cried his wife, impatiently."
7676- />
7777- <TextBlock
7878- lines={1}
7979- defaultValue="“You want to tell me, and I have no objection to hearing it.”"
8080- />
8181- <TextBlock lines={1} defaultValue="" />
8282- {/* <h4>Related Links</h4>
8383- <h3>Related Images</h3>
8484- <ImageBlock src="./test-image.jpg" cardHeight={props.cardHeight} />
8585- <ImageBlock src="./test-image-2.jpg" cardHeight={props.cardHeight} /> */}
8686- <ExternalLinkBlock />
8787-8888- <CardBlock screenshot="./card1.png" title="Chapter 2" />
8989-9090- <CardBlock
9191- screenshot="./card2.png"
9292- title="Notes"
9393- body="This is me just sort of blabbing on and on and on so that i can make htis thing wrap enough lines and see what it looks like. indeed blah blah blah thats what i have to say about it big yada yada energy"
9494- />
9595- <CardBlock
9696- screenshot="./card3.png"
9797- title="Footnote #3"
9898- body="what if first block very short?"
9999- />
100100- <CardBlock
101101- screenshot="./card5.png"
102102- body="This was invitation enough. “Why, my dear, you must know, Mrs. Long says
103103- that Netherfield is taken by a young man of large fortune from the north
104104- of England; that he came down on Monday in a chaise and four to see the
105105- place, and was so much delighted with it that he agreed with Mr. Morris
106106- immediately; that he is to take possession before Michaelmas, and some
107107- of his servants are to be in the house by the end of next week.”"
108108- />
109109- </div>
110110- );
111111-};
112112-113113-const AddCardButton = (props: {
114114- setCards: ([]) => void;
115115- setFocusedCardIndex: (index: number) => void;
116116- cards: number[];
117117- card: number;
118118- index: number;
119119-}) => {
120120- return (
121121- <ButtonPrimary
122122- onClick={() => {
123123- //add a new card after this one
124124- props.setCards([...props.cards, props.card + 1]);
125125-126126- // focus the new card
127127- props.setFocusedCardIndex(props.index + 1);
128128-129129- //scroll the new card into view
130130- setTimeout(() => {
131131- let newCardID = document.getElementById((props.index + 1).toString());
132132- newCardID?.scrollIntoView({
133133- behavior: "smooth",
134134- inline: "nearest",
135135- });
136136- }, 100);
137137- }}
138138- >
139139- add card
140140- </ButtonPrimary>
141141- );
142142-};
143143-144144-const RemoveCardButton = (props: {
145145- setCards: ([]) => void;
146146- cards: number[];
147147- index: number;
148148-}) => {
149149- return (
150150- <ButtonPrimary
151151- onClick={() => {
152152- props.cards.splice(props.index, 1);
153153- props.setCards([...props.cards]);
154154- }}
155155- >
156156- remove card
157157- </ButtonPrimary>
158158- );
159159-};
160160-161161-const FocusCardButton = (props: {
162162- setFocusedCardIndex: (index: number) => void;
163163- index: number;
164164- cardWidth: number;
165165-}) => {
166166- return (
167167- <ButtonPrimary
168168- onClick={() => {
169169- //set the focused card to this one
170170- props.setFocusedCardIndex(props.index);
171171-172172- // check if the card is off screen to the right or left
173173- let cardPosition =
174174- document
175175- .getElementById(props.index.toString())
176176- ?.getBoundingClientRect().left || 0;
177177- let isOffScreenLeft = cardPosition < 0;
178178- let isOffScreenRight =
179179- cardPosition + props.cardWidth > window.innerWidth;
180180-181181- //if card is off screen, scroll one card width to the left or right so that the card is in view
182182- setTimeout(() => {
183183- document.getElementById("card-carousel")?.scrollBy({
184184- top: 0,
185185- left: isOffScreenLeft
186186- ? -props.cardWidth
187187- : isOffScreenRight
188188- ? props.cardWidth
189189- : 0,
190190- behavior: "smooth",
191191- });
192192- }, 100);
193193- }}
194194- >
195195- focus this card
196196- </ButtonPrimary>
197197- );
198198-};