react-intersection-observer#useInView TypeScript Examples
The following examples show how to use
react-intersection-observer#useInView.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: animated-counter.tsx From web with Apache License 2.0 | 7 votes |
AnimatedCounter = ({ countTo }: PropTypes) => {
const [ref, inView] = useInView({ delay: 300, triggerOnce: true })
return (
<div ref={ref}>
{inView ? (
<CountUp
delay={0}
start={0}
end={countTo}
useEasing
duration={3}
formattingFn={(v) => numeral(v).format('0.0a')}
>
{({ countUpRef }) => <span ref={countUpRef} />}
</CountUp>
) : (
<span>0</span>
)}
</div>
)
}
Example #2
Source File: useInViewAnimate.ts From framer-motion-hooks with MIT License | 6 votes |
useInViewAnimate = (
{ initial, animate }: IStates,
options: IntersectionOptions = {}
) => {
const animation = useAnimation()
const [inViewRef, inView] = useInView(options)
useEffect(() => {
if (initial) animation.set(initial)
}, [])
useEffect(() => {
if (inView) {
animation.start(animate)
} else if (initial && options.triggerOnce === false) {
animation.start(initial)
}
}, [inView])
return { inViewRef, animation }
}
Example #3
Source File: ab-animation.tsx From basement-grotesque with SIL Open Font License 1.1 | 5 votes |
AbAnimation = () => {
const { ref, inView } = useInView({ threshold: 0.1, triggerOnce: true })
const tlRef = useRef<gsap.core.Timeline>()
useEffect(() => {
gsap.set('.ab svg', { autoAlpha: 0 })
}, [])
useEffect(() => {
if (!inView) return
function handleResize() {
const multiplier = Math.min((window.innerWidth * 20) / 1440, 20)
const tl = gsap.timeline({
paused: true,
smoothChildTiming: true
})
tl.to('.ab svg', {
delay: DURATION / 2,
y: (index) => index * multiplier,
x: (index) => index * multiplier,
autoAlpha: 1,
ease: 'elastic.out(1, 0.75)',
duration: DURATION,
stagger: {
each: 0.02,
from: 'end'
}
})
.timeScale(0.4)
.play()
tlRef.current = tl
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [inView])
useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
tlRef.current?.kill()
}
}, [])
return (
<Box
css={{
position: 'relative',
width: '100%',
marginTop: '-64px',
'.absolute': { position: 'absolute', inset: 0 }
}}
ref={ref}
>
<Ab1 />
<Ab2 />
<Ab3 />
<Ab4 />
</Box>
)
}
Example #4
Source File: useTrackImpression.ts From apps with GNU Affero General Public License v3.0 | 5 votes |
export default function useTrackImpression(
item: FeedItem,
index: number,
columns: number,
column: number,
row: number,
feedName: string,
ranking?: string,
): (node?: Element | null) => void {
const { trackEventStart, trackEventEnd } = useContext(AnalyticsContext);
const { ref: inViewRef, inView } = useInView({
threshold: 0.5,
});
useEffect(() => {
if (item.type === 'post') {
const eventKey = `pi-${item.post.id}`;
if (inView && !item.post.impressionStatus) {
trackEventStart(
eventKey,
postAnalyticsEvent('impression', item.post, {
columns,
column,
row,
...feedAnalyticsExtra(feedName, ranking, {
scroll_y: window.scrollY,
}),
}),
);
// eslint-disable-next-line no-param-reassign
item.post.impressionStatus = TRACKING;
} else if (!inView && item.post.impressionStatus === TRACKING) {
trackEventEnd(eventKey);
// eslint-disable-next-line no-param-reassign
item.post.impressionStatus = TRACKED;
}
} else if (item.type === 'ad') {
const eventKey = `ai-${index}`;
if (inView && !item.ad.impressionStatus) {
trackEventStart(
eventKey,
adAnalyticsEvent('impression', item.ad, {
columns,
column,
row,
...feedAnalyticsExtra(feedName, ranking),
}),
);
// eslint-disable-next-line no-param-reassign
item.ad.impressionStatus = TRACKING;
} else if (!inView && item.ad.impressionStatus === TRACKING) {
trackEventEnd(eventKey);
// eslint-disable-next-line no-param-reassign
item.ad.impressionStatus = TRACKED;
}
}
}, [inView]);
useEffect(() => {
// Send pending impression on unmount
return () => {
if (item.type === 'ad' && item.ad.impressionStatus === TRACKING) {
const eventKey = `ai-${index}`;
trackEventEnd(eventKey);
// eslint-disable-next-line no-param-reassign
item.ad.impressionStatus = TRACKED;
} else if (
item.type === 'post' &&
item.post.impressionStatus === TRACKING
) {
const eventKey = `pi-${item.post.id}`;
trackEventEnd(eventKey);
// eslint-disable-next-line no-param-reassign
item.post.impressionStatus = TRACKED;
}
};
}, []);
return inViewRef;
}
Example #5
Source File: useFeedInfiniteScroll.ts From apps with GNU Affero General Public License v3.0 | 5 votes |
export default function useFeedInfiniteScroll({
fetchPage,
canFetchMore,
}: UseFeedInfiniteScrollProps): (node?: Element | null) => void {
const { ref: infiniteScrollRef, inView } = useInView({
rootMargin: '20px',
threshold: 1,
});
useEffect(() => {
if (inView && canFetchMore) {
fetchPage();
}
}, [inView, canFetchMore]);
return infiniteScrollRef;
}
Example #6
Source File: rome-preview.tsx From basement-grotesque with SIL Open Font License 1.1 | 4 votes |
RomePreview = () => {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 })
useEffect(() => {
gsap.set('.rome-preview__section', {
autoAlpha: 0
})
}, [])
useEffect(() => {
if (!inView) return
const title = new SplitText('.rome-preview__title', {
type: 'lines,words,chars'
})
const subtitle = new SplitText('.rome-preview__subtitle', {
type: 'lines,words,chars'
})
const items = document.querySelectorAll('.rome-preview__items')[0]
.childNodes
const tl = gsap.timeline({
paused: true,
smoothChildTiming: true
})
tl.to('.rome-preview__section', {
autoAlpha: 1,
duration: DURATION / 2
})
tl.in(title.words)
tl.from(
'.rome-preview__svg',
{
autoAlpha: 0,
y: 30
},
'< 30%'
)
tl.in(subtitle.lines, '>-30%')
tl.from(items, {
autoAlpha: 0,
y: 30,
stagger: 0.1
})
tl.timeScale(1.2).play()
return () => {
tl.kill()
}
}, [inView])
return (
<Section background="black" css={{ pt: '64px' }}>
<Container
className="rome-preview__section"
css={{ maxWidth: '1800px', mx: 'auto' }}
ref={ref}
>
<Text size="icon" centered className="rome-preview__svg">
†
</Text>
<ContentContainer size="sm" centered>
<Text
className="rome-preview__title"
size="lg"
css={{ marginTop: 20, textTransform: 'uppercase', lineHeight: 1.2 }}
centered
>
Lëtzebuerg
<br />
Moscow
<br />
Genève
<br />
Roma
</Text>
</ContentContainer>
<Text
size="icon"
css={{ marginTop: 24 }}
centered
className="rome-preview__svg"
>
‡
</Text>
<Divisor />
<ContentContainer size="lg" centered>
<ContentContainer size="sm">
<Text size="md" className="rome-preview__subtitle">
CATACOMBS, ALTHOUGH MOST NOTABLE AS UNDERGROUND PASSAGEWAYS AND
CEMETERIES, ALSO HOUSE MANY DECORATIONS.
</Text>
</ContentContainer>
<ColumnedContent
css={{ marginTop: 80 }}
className="rome-preview__items"
>
<ContentContainer
css={{
background: '$background',
color: '#FDFDFD',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
maxHeight: 168
}}
>
<span>
<svg
width="119"
height="168"
viewBox="0 0 119 168"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M55.9082 91.3575C56.2204 97.9133 49.9768 106.342 38.1141 109.464L25.0027 107.279C-5.59058 101.972 -11.8341 96.9767 -11.8341 85.4262C-12.1463 78.8705 -5.90276 70.4417 5.95995 67.32L19.0714 69.5052C49.6647 74.8122 55.9082 79.807 55.9082 91.3575ZM-74.2694 94.7915C-74.2694 113.834 -67.0893 130.692 -37.1204 139.433C-21.1994 144.115 -6.83928 147.861 26.8758 152.856C47.1673 155.978 55.9082 159.724 55.9082 171.899C55.9082 186.884 52.7864 188.444 23.4418 188.444C-6.83928 188.444 -10.2732 185.947 -10.2732 156.915H-69.5868C-69.5868 215.604 -50.544 229.027 26.2514 229.027C99.9251 229.027 118.343 217.165 118.343 169.402C118.343 144.428 111.788 130.38 86.1893 120.702C83.0675 119.453 79.3214 118.205 75.5753 117.268C106.481 116.956 118.343 106.342 118.343 81.9922C118.343 62.9495 111.163 46.0919 81.1945 37.351C65.2735 32.6684 50.9134 28.9222 17.1983 23.9274C-3.09317 20.8056 -11.8341 17.0595 -11.8341 4.88465C-11.8341 -10.0998 -8.71234 -11.6607 20.6322 -11.6607C50.9134 -11.6607 54.3473 -9.16329 54.3473 19.8691H113.661C113.661 -38.82 94.6181 -52.2437 17.8227 -52.2437C-55.851 -52.2437 -74.2694 -40.3809 -74.2694 7.38207C-74.2694 32.3562 -67.7137 46.4041 -42.1152 56.0816C-38.9935 57.3303 -35.2473 58.579 -31.5012 59.5155C-63.3432 59.8277 -74.2694 70.4417 -74.2694 94.7915Z"
fill="#FDFDFD"
/>
</svg>
</span>
<Text css={{ fontSize: '$13', lineHeight: 1, marginRight: 24 }}>
¶
</Text>
</ContentContainer>
<div>
<Text size="sm">
There are thousands of decorations in the centuries-old
catacombs of Rome, catacombs of Paris, and other known and
unknown catacombs, some of which include inscriptions,
paintings, statues, ornaments, and other items placed in the
graves over the years.
</Text>
</div>
</ColumnedContent>
</ContentContainer>
</Container>
<Container
css={{ background: '$background', '@bp1': { background: 'black' } }}
maxWidth
>
<Container
css={{
mt: '64px',
background: '$background',
padding: 0,
'@bp1': {
padding: `min(40px, ${toVw(40)})`
}
}}
>
<Box
css={{
display: 'grid',
'@bp1': { gridTemplateColumns: '1fr 1fr' }
}}
>
{[
{
label: (
<>
MANY ACCENTS,
<br />
OBVIOUSLY
</>
),
children: (
<Box data-scroll data-scroll-speed={0.35}>
<span>
<Box
data-scroll
data-scroll-speed={0.1}
css={{ display: 'inline-block' }}
as="span"
>
a
</Box>
<Box
data-scroll
data-scroll-speed={0.2}
css={{ display: 'inline-block' }}
as="span"
>
à
</Box>
<Box
data-scroll
data-scroll-speed={0.3}
css={{ display: 'inline-block' }}
as="span"
>
á
</Box>
<Box
data-scroll
data-scroll-speed={0.4}
css={{ display: 'inline-block' }}
as="span"
>
â
</Box>
<Box
data-scroll
data-scroll-speed={0.5}
css={{ display: 'inline-block' }}
as="span"
>
ä
</Box>
</span>
<br />
<span className="ss01">
<Box
data-scroll
data-scroll-speed={0.1}
css={{ display: 'inline-block' }}
as="span"
>
a
</Box>
<Box
data-scroll
data-scroll-speed={0.2}
css={{ display: 'inline-block' }}
as="span"
>
à
</Box>
<Box
data-scroll
data-scroll-speed={0.3}
css={{ display: 'inline-block' }}
as="span"
>
á
</Box>
<Box
data-scroll
data-scroll-speed={0.4}
css={{ display: 'inline-block' }}
as="span"
>
â
</Box>
<Box
data-scroll
data-scroll-speed={0.5}
css={{ display: 'inline-block' }}
as="span"
>
ä
</Box>
</span>
<br />
<span className="ss02">
<Box
data-scroll
data-scroll-speed={0.1}
css={{ display: 'inline-block' }}
as="span"
>
a
</Box>
<Box
data-scroll
data-scroll-speed={0.2}
css={{ display: 'inline-block' }}
as="span"
>
à
</Box>
<Box
data-scroll
data-scroll-speed={0.3}
css={{ display: 'inline-block' }}
as="span"
>
á
</Box>
<Box
data-scroll
data-scroll-speed={0.4}
css={{ display: 'inline-block' }}
as="span"
>
â
</Box>
<Box
data-scroll
data-scroll-speed={0.5}
css={{ display: 'inline-block' }}
as="span"
>
ä
</Box>
</span>
</Box>
)
},
{
label: (
<>
CAPITAL VARIABLES,
<br />
BECAUSE, WHY NOT?
</>
),
background: 'white',
color: 'black',
invertSelection: true,
children: (
<>
<span>
<Box
data-scroll
data-scroll-speed={0.1}
css={{ display: 'inline-block' }}
as="span"
>
R
</Box>
</span>
<span className="ss01">
<Box
data-scroll
data-scroll-speed={0.2}
css={{ display: 'inline-block' }}
as="span"
>
R
</Box>
</span>
<span className="ss02">
<Box
data-scroll
data-scroll-speed={0.3}
css={{ display: 'inline-block' }}
as="span"
>
R
</Box>
</span>
<span className="ss03">
<Box
data-scroll
data-scroll-speed={0.4}
css={{ display: 'inline-block' }}
as="span"
>
R
</Box>
</span>
<br />
<span>
<Box
data-scroll
data-scroll-speed={0.1}
css={{ display: 'inline-block' }}
as="span"
>
G
</Box>
</span>
<span className="ss01">
<Box
data-scroll
data-scroll-speed={0.2}
css={{ display: 'inline-block' }}
as="span"
>
G
</Box>
</span>
<br />
<span>
<Box
data-scroll
data-scroll-speed={0.1}
css={{ display: 'inline-block' }}
as="span"
>
Q
</Box>
</span>
<span className="ss01">
<Box
data-scroll
data-scroll-speed={0.2}
css={{ display: 'inline-block' }}
as="span"
>
Q
</Box>
</span>
</>
)
},
{
label: null,
hideOnDesktop: true,
children: (
<>
<span>1918-2021</span>
<br />
<span>£ 206.10</span>
<br />
<span>€ 37,00</span>
</>
)
}
].map(
(
{
label,
background,
color,
children,
invertSelection,
hideOnDesktop
},
i
) => {
return (
<StyledAspectBox
key={i}
ratio={680 / 587}
className={invertSelection ? 'invert-selection' : ''}
css={{
background,
color,
position: 'relative',
label: {
position: 'absolute',
top: `min(24px, ${toVw(24)})`,
right: `min(32px, ${toVw(32)})`,
textAlign: 'right',
fontWeight: 500,
lineHeight: 1.25,
userSelect: 'none',
fontSize: `min(16px, ${toVw(16)})`,
display: 'none'
},
'@bp1': {
display: hideOnDesktop ? 'none' : 'block',
label: {
display: 'block'
}
}
}}
>
<label>{label}</label>
<Box
className="children"
css={{
width: '100%',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontFamily: '$heading',
letterSpacing: '0.04em',
lineHeight: 1,
textAlign: 'center',
fontSize: `min(56px, ${toVw(52, 375)})`,
'@bp1': {
fontSize: `min(112px, ${toVw(112)})`
}
}}
>
{children}
</Box>
</StyledAspectBox>
)
}
)}
</Box>
<Box
css={{
display: 'none',
textAlign: 'center',
py: `min(120px, ${toVw(120)})`,
mt: `min(40px, ${toVw(40)})`,
label: {
fontWeight: 500,
lineHeight: 1.25,
userSelect: 'none',
mt: '24px',
fontSize: `min(16px, ${toVw(16)})`,
display: 'none'
},
'@bp1': {
display: 'block',
label: {
display: 'block'
}
}
}}
>
<Box
css={{
fontFamily: '$heading',
letterSpacing: '0.04em',
lineHeight: 1,
fontSize: `min(56px, ${toVw(55, 375)})`,
'@bp1': {
fontSize: `min(112px, ${toVw(112)})`
}
}}
>
<Box data-scroll data-scroll-speed={0.1}>
<span>1918-2021</span>
</Box>
<Box data-scroll data-scroll-speed={0.4}>
<span>£ 206.10</span>
</Box>
<Box data-scroll data-scroll-speed={0.7}>
<span>€ 37,00</span>
</Box>
</Box>
<label data-scroll data-scroll-speed={0.4}>
WHO NEEDS NUMBERS?
</label>
</Box>
</Container>
</Container>
</Section>
)
}
Example #7
Source File: Testing.tsx From mswjs.io with MIT License | 4 votes |
Testing = () => {
const [isLoading, setLoading] = useState(true)
const [ref, isContainerVisible] = useInView({
threshold: 1,
})
useEffect(() => {
if (isContainerVisible) {
const timeout = setTimeout(() => {
setLoading(false)
}, 1000)
return () => {
clearTimeout(timeout)
}
}
}, [isContainerVisible])
return (
<ObliqueSection>
<Section>
<Box width="100%">
<Box ref={ref} marginBottom={24}>
<Browser
address="eshop.com/product/1"
maxWidth={500}
marginHorizontal="auto"
>
<Box
flex
heightMd={250}
padding={32}
alignItems="center"
justifyContent="center"
>
<PageContent isLoading={isLoading} />
</Box>
<Box as={BrowserDevTools} padding={20}>
<TestResults>
<TestDescribe>
given I wrote a test suite with MSW
</TestDescribe>
<TestResult
title="treats API response as a pre-requisite"
isLoading={isLoading}
/>
<TestResult
title="tests how a user actually interacts with my app"
isLoading={isLoading}
delay={850}
/>
<TestResult
title="produces a maintainable and resilient test"
isLoading={isLoading}
delay={1700}
/>
</TestResults>
</Box>
</Browser>
</Box>
<TextSmall align="center" color="gray">
Test suite using a <code>GET /product/:productId</code> mock.
</TextSmall>
</Box>
<SectionContent>
<Heading level={2} marginBottom={8} align="center" alignLg="start">
Test with confidence
</Heading>
<TextLead align="center" alignLg="start">
Write test suites that <Accent>don't smell</Accent>.
</TextLead>
<Text color="gray">
You don't expect your customers to mock <code>fetch</code>, do you?
So don't expect your tests either. Target any state of your API
while testing your application exactly how your users interact with
it.
</Text>
<ReadmoreLink
href="https://kentcdodds.com/blog/stop-mocking-fetch"
target="_blank"
rel="noopener noreferrer"
>
Learn about API mocking in tests with Kent C. Dodds
</ReadmoreLink>
</SectionContent>
</Section>
</ObliqueSection>
)
}
Example #8
Source File: useArt.ts From metaplex with Apache License 2.0 | 4 votes |
useExtendedArt = (id?: StringPublicKey) => {
const { metadata } = useMeta();
const [data, setData] = useState<IMetadataExtension>();
const { width } = useWindowDimensions();
const { ref, inView } = useInView({ root: null, rootMargin: '-100px 0px' });
const localStorage = useLocalStorage();
const key = pubkeyToString(id);
const account = useMemo(
() => metadata.find(a => a.pubkey === key),
[key, metadata],
);
useEffect(() => {
if ((inView || width < 768) && id && !data) {
const USE_CDN = false;
const routeCDN = (uri: string) => {
let result = uri;
if (USE_CDN) {
result = uri.replace(
'https://arweave.net/',
'https://coldcdn.com/api/cdn/bronil/',
);
}
return result;
};
if (account && account.info.data.uri) {
const uri = routeCDN(account.info.data.uri);
const processJson = (extended: any) => {
if (!extended || extended?.properties?.files?.length === 0) {
return;
}
if (extended?.image) {
const file = extended.image.startsWith('http')
? extended.image
: `${account.info.data.uri}/${extended.image}`;
extended.image = routeCDN(file);
}
return extended;
};
try {
const cached = localStorage.getItem(uri);
if (cached) {
setData(processJson(JSON.parse(cached)));
} else {
// TODO: BL handle concurrent calls to avoid double query
fetch(uri)
.then(async _ => {
try {
const data = await _.json();
try {
localStorage.setItem(uri, JSON.stringify(data));
} catch {
// ignore
}
setData(processJson(data));
} catch {
return undefined;
}
})
.catch(() => {
return undefined;
});
}
} catch (ex) {
console.error(ex);
}
}
}
}, [inView, id, data, setData, account]);
return { ref, data };
}
Example #9
Source File: Page.tsx From react-pdf-ner-annotator with MIT License | 4 votes |
Page = ({
pageNumber,
shouldRender,
page,
scale,
annotations,
addAnnotation,
updateLastAnnotationForEntity,
addPageToTextMap,
initialTextLayer,
}: Props) => {
const {
config: { disableOCR },
} = useContext(ConfigContext);
const { tokenizer } = useContext(AnnotationContext);
const [inViewRef, inView] = useInView({ threshold: 0 });
const canvasRef = useRef<HTMLCanvasElement>(null);
const [loading, setLoading] = useState(true);
const [pdfPage, setPdfPage] = useState<PDFPageProxy | null>(null);
const [context, setContext] = useState<CanvasRenderingContext2D | null>(null);
const [startOcr, setStartOcr] = useState(false);
const [pageViewport, setPageViewport] = useState<any>({ width: (916 / 1.5) * scale, height: (1174 / 1.5) * scale });
const { textLayer, buildTextLayer } = useTextLayer(scale, context!, initialTextLayer);
const { ocrResult, ocrError, ocrLoading, doOCR } = useTesseract(scale, context!);
const message = ocrResult ? `OCR confidence ${ocrResult.confidence}%` : undefined;
useEffect(() => {
if (annotations.length) {
if (textLayer) {
addPageToTextMap(pageNumber, textLayer, TextLayerType.TEXT_LAYER, 1, tokenizer);
return;
}
if (ocrResult) {
addPageToTextMap(pageNumber, ocrResult.ocrWords, TextLayerType.ORC, ocrResult.confidence);
}
}
}, [annotations, textLayer, ocrResult, pageNumber, addPageToTextMap, tokenizer]);
useEffect(() => {
if (!disableOCR && startOcr && inView && !ocrResult) {
doOCR();
}
}, [disableOCR, startOcr, inView, doOCR, ocrResult]);
useEffect(() => {
if (canvasRef) {
setContext(canvasRef.current!.getContext('2d'));
}
}, [canvasRef]);
useEffect(() => {
if (canvasRef && context && page && inView) {
page.then((pdfPageResult) => {
const viewport = pdfPageResult.getViewport({ scale });
const { width, height } = viewport;
setPageViewport(viewport);
const canvas = canvasRef.current;
canvas!.width = width;
canvas!.height = height;
pdfPageResult
.render({
canvasContext: context!,
viewport,
})
.promise.then(() => {
setPdfPage(pdfPageResult);
});
});
}
}, [page, scale, canvasRef, context, inView]);
useEffect(() => {
if (textLayer?.length) {
setLoading(false);
return;
}
if (inView && pdfPage && !textLayer) {
pdfPage.getTextContent().then((content) => {
if (content.items.length) {
const contentMerged = mergeSplitWords(content);
buildTextLayer(contentMerged, pageViewport as PDFPageViewport);
} else {
setStartOcr(true);
}
setLoading(false);
});
}
}, [inView, pdfPage, pageViewport, context, page, textLayer, buildTextLayer]);
return (
<div className="page" ref={inViewRef}>
<div className="page__container" style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }}>
<div
className="page__canvas-container"
style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }}
>
{loading ? <Loader /> : null}
<canvas ref={canvasRef} style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }} />
</div>
<Selection
pageNumber={pageNumber}
className="page__text-layer-container"
style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }}
addAnnotation={addAnnotation}
updateLastAnnotationForEntity={updateLastAnnotationForEntity}
pdfInformation={{ width: pageViewport.width, height: pageViewport.height, scale }}
pdfContext={context}
>
<TextLayer
inView={inView}
shouldRender={shouldRender}
canvasInitialized={!!canvasRef}
textLayer={textLayer || ocrResult?.ocrWords}
pageNumber={pageNumber}
needsTokenization={!initialTextLayer}
/>
<AreaLayer pdfScale={scale} pageNumber={pageNumber} />
<div className="ocr-info-container">
<OcrInfo loading={ocrLoading} message={message} error={ocrError} />
</div>
</Selection>
</div>
</div>
);
}
Example #10
Source File: Wrapper.tsx From yugong with MIT License | 4 votes |
Wrapper: React.FC<Props> = ({
style,
children,
layout,
maxWidth,
maxHeight,
moduleId,
itemAlign = 'center',
visible = true
}) => {
// Wrapper 自身的样式
const [basicStyle, setBasicStyle] = useState<{ [keys: string]: any }>({});
const actId = useSelector((state: RootState) => state.controller.editingId);
const [wrapSize, setWrapSize] = useState<{ width: string; height: string }>();
const setEditingId = useDispatch<Dispatch>().controller.setEditingId;
const currentLayout = useSelector((state: RootState) => state.appData).filter(
(item) => item.moduleId === actId,
)?.[0]?.layout;
const refWrap = useRef<HTMLDivElement>(null);
const [ref, inView] = useInView({
threshold: 0,
});
const isEditing = useSelector(
(state: RootState) => state.controller.isEditing,
);
const sendMessage = usePostMessage(() => {});
// base元素的样式控制
const { basic } = style;
const { display, animation } = basic || {};
useEffect(() => {
if (display?.zIndex !== undefined && refWrap.current?.parentElement) {
refWrap.current.parentElement.style.zIndex = `${display.zIndex}`;
}
}, [moduleId, display?.zIndex]);
// 样式编译
useEffect(() => {
setBasicStyle(styleCompiler({ ...basic }, inView));
}, [moduleId, basic, inView]);
// 仅针对有延时的入场动画在延时期间做元素隐藏处理,进入动画再做呈现
const timer = useRef<NodeJS.Timeout | null>();
useEffect(() => {
if (!animation?.animationDelay) return;
refWrap.current?.classList.add(s.hide);
if (timer.current) window.clearTimeout(timer.current);
timer.current = setTimeout(
() => refWrap.current?.classList.remove(s.hide),
animation.animationDelay + 50,
);
}, [basic, animation, inView]);
// 计算尺寸
useEffect(() => {
if (refWrap.current) {
setWrapSize({
width: `${refWrap.current.offsetWidth}px`,
height: `${refWrap.current.offsetHeight}px`,
});
}
}, [refWrap, currentLayout?.w, currentLayout?.h]);
/**
* 图层被触发
*/
const onLayoutClick = useCallback(() => {
if (!isEditing) return;
setEditingId(moduleId);
// 向父级窗口通知当前激活Id
sendMessage({ tag: 'id', value: moduleId }, window.top);
}, [isEditing, moduleId, sendMessage, setEditingId]);
/**设置预览状态下不接受编辑事件 */
const pointerEvents: React.CSSProperties = {};
if (isEditing) {
pointerEvents.pointerEvents = 'none';
} else {
delete pointerEvents.pointerEvents;
}
/*设置最大尺寸*/
const defaultSize: AnyObjectType = {};
if (maxWidth && wrapSize?.width) {
defaultSize.width = wrapSize?.width;
}
if (maxHeight && wrapSize?.height) {
defaultSize.height = wrapSize?.height;
}
/*是否为隐藏模块*/
const isHide = layout?.w === 0 || layout?.h === 0;
if (isHide) {
defaultSize.width = defaultSize.height = 'auto';
}
if (visible === false) return null;
return (
<div
className={classNames(s.touchwrap, {
[s.aligncenter]: itemAlign === 'center',
[s.aligntop]: itemAlign === 'top',
[s.alignbottom]: itemAlign === 'bottom',
})}
onTouchStart={onLayoutClick}
onMouseDown={onLayoutClick}
ref={refWrap}
>
{actId === moduleId ? (
<div
className={classNames(s.actwrap, {
[s.isedit]: isEditing,
[s.iswiew]: !isEditing,
})}
/>
) : null}
<div ref={ref} className={s.animationwrap} style={{ ...defaultSize }}>
<div
id={moduleId}
className={s.secondwrap}
style={{ ...defaultSize, ...basicStyle.style, ...pointerEvents }}
>
{children}
</div>
</div>
</div>
);
}
Example #11
Source File: LandingTidbits.tsx From frontend.ro with MIT License | 4 votes |
LandingTidbits = ({ tidbits }: { tidbits: TidbitI[] }) => {
const { ref, inView } = useInView({
threshold: 0.4,
triggerOnce: true,
});
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (!inView) {
return noop;
}
const TIMEOUT_DURATION = 3000;
const timeoutId = setTimeout(nextPage, TIMEOUT_DURATION);
return () => {
clearTimeout(timeoutId);
};
}, [currentIndex, inView]);
const nextPage = () => {
if (currentIndex + 1 === tidbits.length) {
setCurrentIndex(0);
} else {
setCurrentIndex(currentIndex + 1);
}
};
const previousPage = () => {
if (currentIndex === 0) {
setCurrentIndex(tidbits.length - 1);
} else {
setCurrentIndex(currentIndex - 1);
}
};
return (
<section
className={`
${styles.LandingTidbits}
d-flex
align-items-center
justify-content-between
`}
style={{
backgroundColor: tidbits[currentIndex].backgroundColor,
color: tidbits[currentIndex].textColor,
}}
ref={ref}
>
<div className={`${styles.about} d-flex flex-column justify-content-between`}>
<a
rel="noreferrer"
href={`/tidbits/${tidbits[currentIndex].tidbitId}/${currentIndex + 1}`}
className={`${styles['heading-link']} no-underline`}
>
<h2 className={`${styles.heading} m-0`}>
{tidbits[currentIndex].title}
</h2>
</a>
<div className={styles.controls}>
<div>
<Button variant="transparent" onClick={previousPage}>
<FontAwesomeIcon width={32} icon={faArrowAltCircleLeft} />
</Button>
<Button variant="transparent" onClick={nextPage}>
<FontAwesomeIcon width={32} icon={faArrowAltCircleRight} />
</Button>
</div>
{inView && (
<Progress key={currentIndex} color={tidbits[currentIndex].textColor} />
)}
</div>
</div>
<StackedImages
images={tidbits.map((t, index) => ({
href: `/tidbits/${tidbits[currentIndex].tidbitId}/${currentIndex + 1}`,
alt: t.title,
src: t.items[1].imageSrc,
}))}
rotationDelta={15}
currentIndex={currentIndex}
className={styles.StackedImages}
/>
</section>
);
}
Example #12
Source File: index.tsx From basement-grotesque with SIL Open Font License 1.1 | 4 votes |
DemoSection = () => {
const { fontsLoaded } = useAppContext()
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.05 })
const [inputs, setInputs] = useState<Inputs>({
size: {
label: 'Size',
value: '108',
min: 21,
max: 195,
step: 1,
renderValue: (value) => value + 'PX'
},
tracking: {
label: 'Tracking',
value: '-2',
min: -4,
max: 4,
step: 0.1,
renderValue: (value) => value + 'px'
},
leading: {
label: 'Leading',
value: '110',
min: 83,
max: 140,
step: 1,
renderValue: (value) => value + '%'
}
})
const [text, setText] = useState('We Make Cool Shit That Performs')
const handleChange: RangeProps['onChange'] = useCallback((e) => {
const { name, value } = e.target
const key = name as Name
setInputs((p) => {
return { ...p, [key]: { ...p[key], value } }
})
}, [])
const handleTextChange: React.ChangeEventHandler<HTMLTextAreaElement> =
useCallback((e) => {
setText(e.target.value)
}, [])
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 900) {
setInputs((p) => ({
...p,
tracking: { ...p.tracking, value: '0.4' },
size: { ...p.size, min: 14, max: 100, value: '32' }
}))
} else {
setInputs((p) => ({
...p,
size: { ...p.size, min: 21, max: 195, value: '108' }
}))
}
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
useEffect(() => {
gsap.set('.demo__section', {
autoAlpha: 0
})
}, [])
useEffect(() => {
if (!inView) return
const items = document?.querySelector('.demo__section')?.childNodes
if (!items) return
const tl = gsap.timeline({
paused: true,
smoothChildTiming: true
})
tl.to('.demo__section', {
autoAlpha: 1,
duration: DURATION / 2
})
tl.from(items, {
autoAlpha: 0,
y: 30,
stagger: 0.1
})
tl.play()
return () => {
tl.kill()
}
}, [inView])
return (
<Section
background="black"
css={{
py: 88,
'@bp2': {
zIndex: 10,
py: 128
}
}}
>
<Container withoutPx maxWidth ref={ref}>
<Container className="demo__section">
<SectionHeading
title={<>Try this beauty :)</>}
subtitle={
<Box
as="span"
css={{ display: 'none', '@bp2': { display: 'inline' } }}
>
Handcrafted
<br />
for strong designs
</Box>
}
/>
<Container
css={{
display: 'none',
'@bp2': {
display: 'block'
}
}}
withoutPx
>
<PreviewContainer>
<PreviewLabel>
<p>
{Object.keys(inputs).map((key, i, { length }) => {
const isLast = i === length - 1
const input = inputs[key as Name]
return (
<Fragment key={i}>
{input.label[0]}: {input.renderValue(input.value)}
{isLast ? null : ' | '}
</Fragment>
)
})}
</p>
<ResizableTextarea
value={text}
className={textareaCss}
style={{
fontSize: inputs.size.value + 'px',
lineHeight: inputs.leading.value + '%',
letterSpacing: inputs.tracking.value + 'px',
fontFamily: 'var(--fonts-heading)'
}}
onChange={handleTextChange}
fontsLoaded={fontsLoaded}
/>
</PreviewLabel>
</PreviewContainer>
</Container>
<Box css={{ '@bp2': { display: 'none' } }}>
<PreviewContainer>
<PreviewLabel>
<p>
{Object.keys(inputs).map((key, i, { length }) => {
const isLast = i === length - 1
const input = inputs[key as Name]
return (
<Fragment key={i}>
{input.label[0]}: {input.renderValue(input.value)}
{isLast ? null : ' | '}
</Fragment>
)
})}
</p>
<ResizableTextarea
value={text}
className={textareaCss}
style={{
fontSize: inputs.size.value + 'px',
lineHeight: inputs.leading.value + '%',
letterSpacing: inputs.tracking.value + 'px',
fontFamily: 'var(--fonts-heading)'
}}
onChange={handleTextChange}
fontsLoaded={fontsLoaded}
/>
</PreviewLabel>
</PreviewContainer>
</Box>
<InputsContainer>
{Object.keys(inputs).map((key) => {
return (
<Range
{...inputs[key as Name]}
name={key}
key={key}
onChange={handleChange}
/>
)
})}
</InputsContainer>
</Container>
</Container>
</Section>
)
}
Example #13
Source File: index.tsx From basement-grotesque with SIL Open Font License 1.1 | 4 votes |
CharactersSection = () => {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 })
const [viewAll, setViewAll] = useState(false)
const [gridHeight, setGridHeight] = useState<number>()
const gridRef = useRef<HTMLDivElement>(null)
const handleToggleViewAll = useCallback(() => setViewAll((p) => !p), [])
const handleCopyGlyph: React.MouseEventHandler<HTMLButtonElement> =
useCallback(async (e) => {
const glyph = e.currentTarget.innerText
try {
await navigator.clipboard.writeText(glyph)
toast.success(`Copied to clipboard`, {
icon: glyph,
style: {
borderRadius: '10px',
backgroundColor: 'black',
color: 'white'
}
})
} catch (error) {
toast.error(`Failed to copy ${glyph} to clipboard`, {
style: {
borderRadius: '10px',
backgroundColor: 'black',
color: 'white'
}
})
}
}, [])
useEffect(() => {
const handleResize = () => {
if (!gridRef.current) return
setGridHeight(gridRef.current.offsetHeight)
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
useEffect(() => {
gsap.set('#characters-section', {
autoAlpha: 0
})
}, [])
useEffect(() => {
if (!inView) return
const title = new SplitText('.characters__title', {
type: 'lines,words,chars'
})
const tl = gsap.timeline({
paused: true,
smoothChildTiming: true
})
tl.to('#characters-section', {
autoAlpha: 1,
duration: DURATION / 2
})
tl.in(title.chars)
tl.from(
'.characters__svg',
{
autoAlpha: 0,
y: 30
},
'< 80%'
)
tl.timeScale(1.5).play()
return () => {
tl.kill()
}
}, [inView])
return (
<Section background="black">
<SectionInner
id="characters-section"
autoPy
css={{ pb: viewAll ? '128px' : '0px' }}
maxWidth
ref={ref}
>
<Box
css={{
position: 'relative',
py: '64px',
mb: '64px',
'@bp2': {
py: '128px',
mb: '128px'
}
}}
>
<Box
css={{
position: 'absolute',
pointerEvents: 'none',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}}
>
<svg
className="characters__svg"
width="638"
height="810"
viewBox="0 0 638 810"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M528.721 68.2758C543.566 77.5248 551.979 95.6945 554.372 120.803C556.765 145.901 553.132 177.835 543.988 214.45C525.702 287.673 485.406 379.5 427.425 472.562C369.443 565.624 304.776 642.267 247.108 690.953C218.272 715.299 191.208 732.635 167.624 741.549C144.031 750.466 124.013 750.923 109.168 741.674C94.3235 732.425 85.9114 714.255 83.5175 689.147C81.1245 664.049 84.758 632.115 93.9019 595.5C112.188 522.277 152.484 430.45 210.465 337.388C268.447 244.326 333.114 167.683 390.782 118.997C419.618 94.6513 446.682 77.3151 470.266 68.4012C493.859 59.4839 513.877 59.0269 528.721 68.2758Z"
stroke="white"
/>
</svg>
</Box>
<Box
as="p"
className="characters__title"
css={{
fontFamily: '$heading',
fontSize: '48px',
fontWeight: 800,
wordBreak: 'break-all',
textAlign: 'center',
lineHeight: '1',
maxWidth: '1280px',
margin: 'auto',
'@bp2': {
fontSize: '88px'
}
}}
data-scroll-speed={-0.6}
data-scroll
>
ABCDEFGHIJKLMNOPQRSTUVWXYZ
<br />
abcdefghijklmnopqrstuvwxyz
<br />
0123456789!?&
</Box>
</Box>
<SectionHeading
title="Characters"
subtitle={
<>
413 glyphs
<br />
Black (800)
<br />
OTF
</>
}
/>
<DesktopOnlyBox
css={{
transition: gridHeight ? `height ${gridHeight / 4000}s` : undefined,
height: viewAll ? gridHeight : '570px',
overflow: 'hidden'
}}
>
<Box
ref={gridRef}
css={{
display: 'grid',
gridTemplateColumns: 'repeat(12, 1fr)',
gridColumnGap: `min(18px, ${toVw(16)})`,
gridRowGap: `min(18px, ${toVw(16)})`,
overflow: 'hidden',
pt: '80px',
pb: '30px'
}}
>
{glyphs.split('').map((glyph, i) => (
<Glyph key={i} onClick={handleCopyGlyph}>
{glyph}
</Glyph>
))}
</Box>
</DesktopOnlyBox>
</SectionInner>
<DesktopOnlyBox>
{!viewAll && (
<Box
as="button"
onClick={handleToggleViewAll}
css={{
appearance: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
background: '$white',
color: '$black',
borderTop: '1px solid $black',
py: '24px',
textTransform: 'uppercase',
'&:focus': {
outline: 'none'
},
fontFamily: '$heading',
svg: {
ml: '8px',
transition: 'all 250ms',
fill: 'currentColor',
color: '$black'
}
}}
>
{viewAll ? 'View Less' : 'View All'}{' '}
<ArrowDown
css={{
color: 'black',
path: { stroke: 'black' },
$$size: '15px'
}}
/>
</Box>
)}
</DesktopOnlyBox>
<MobileSection>
<Marquee gradient={false} speed={50}>
<Box
css={{
display: 'grid',
gridTemplateRows: 'repeat(4, 1fr)',
gridColumnGap: '16px',
gridRowGap: '16px',
overflow: 'hidden',
gridAutoFlow: 'column',
mx: '8px',
py: '20px'
}}
>
{mobileGlyphs.split('').map((glyph, i) => (
<Glyph key={i} onClick={handleCopyGlyph}>
{glyph}
</Glyph>
))}
</Box>
</Marquee>
</MobileSection>
</Section>
)
}
Example #14
Source File: index.tsx From basement-grotesque with SIL Open Font License 1.1 | 4 votes |
AboutSection = () => {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 })
useEffect(() => {
gsap.set('#about-section', {
autoAlpha: 0
})
}, [])
useEffect(() => {
if (!inView) return
const title = new SplitText('.about__title', {
type: 'lines,words,chars'
})
const subtitle = new SplitText('.about__subtitle', {
type: 'lines'
})
const tl = gsap.timeline({
paused: true,
smoothChildTiming: true
})
tl.to('#about-section', {
autoAlpha: 1,
duration: DURATION / 2
})
tl.in(title.lines)
tl.in(subtitle.lines, '<40%')
tl.timeScale(1.3).play()
return () => {
tl.kill()
}
}, [inView])
return (
<Section background="black" css={{ pt: '128px' }} id="about-section">
<Container autoPy css={{ pb: 0 }} maxWidth ref={ref}>
<Box
css={{
display: 'grid',
gridTemplateColumns: '1fr',
gap: '64px',
'@bp2': {
gridTemplateColumns: '1fr 1fr'
}
}}
>
<Box
data-scroll-speed={0.6}
data-scroll
css={{
maxWidth: 708,
gridRowStart: 2,
'@bp2': { gridRowStart: 'initial' }
}}
>
<Text
className="about__title"
size="bg"
css={{
marginBottom: 48,
'@bp2': { marginBottom: 28 }
}}
>
<b>Basement Grotesque</b> is the studio’s first venture into the
daunting but exciting world of type design. Of course, we had to
start with a heavyweight: striking and unapologetically so; flawed
but charming and full of character.
</Text>
<Text
className="about__subtitle"
css={{
mb: 16
}}
>
We set out inspired by the expressiveness of early 19th-century
grotesque typefaces and the boldness and striking visuals of the
contemporary revival of brutalist aesthetics. Grotesque is the
first step in a very ambitious path we’ve set for ourselves.
</Text>
<Text className="about__subtitle">
The typeface is a work in progress, open to anyone who shares our
visual and graphic sensibilities. You're invited to check our
journey as we iterate, change, and add new weights and widths in
the future as we learn by doing.
</Text>
</Box>
<Box
css={{
display: 'flex',
alignItems: 'center',
'@bp2': { margin: '-128px -128px -128px -32px' }
}}
>
<AbAnimation />
</Box>
</Box>
<Box
data-scroll-speed={0.1}
data-scroll
css={{
ta: 'center',
fontWeight: 800,
fontFamily: '$heading',
fontSize: 'clamp($4, 2vw, $7)',
lineHeight: 'clamp($5, 2.2vw, $8)',
mt: '64px',
'@bp2': {
mt: '128px'
}
}}
>
***
</Box>
</Container>
</Section>
)
}
Example #15
Source File: index.tsx From basement-grotesque with SIL Open Font License 1.1 | 4 votes |
Footer = () => {
const { inView, ref } = useInView({ triggerOnce: true })
return (
<Section
css={{
paddingBottom: 40,
paddingTop: 48
}}
background="black"
data-scroll-section
noMargin
>
<Container maxWidth>
<FooterGrid>
<Box
css={{ position: 'relative', overflow: 'hidden' }}
className="fallingLetters"
ref={ref}
>
{inView && <FooterAnimation />}
</Box>
<Box
className="social"
css={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: 20
}}
>
<Box>
<Text
css={{
fontSize: 'min(60px, 5.4vw)',
lineHeight: 1,
'@bp2': { fontSize: 'min(60px, 2.8vw)' }
}}
uppercase
heading
tight
>
Our work is serious,{' '}
<Text
css={{ lineHeight: 1.5, display: 'block' }}
as="span"
heading
outlined
>
we are not.
</Text>
</Text>
</Box>
<Social css={{ marginTop: '$4' }}>
{social.map(({ label, href }, idx) => (
<li key={idx}>
<a href={href} target="_blank" rel="noopener">
<Text
className="label"
css={{
fontSize: '$3',
'@bp2': { fontSize: 'min(24px, 1.3vw)' }
}}
heading
uppercase
>
{label}
</Text>
<Box
css={{
width: 10,
height: 10,
'@bp2': { width: 19, height: 19 }
}}
className="arrow"
>
<ArrowUp
css={{
$$size: '$3',
'@bp2': { $$size: 'min(20px, 1.3vw)' }
}}
/>
</Box>
</a>
</li>
))}
</Social>
</Box>
<Box
className="policies"
css={{
display: 'flex',
justifyContent: 'space-between',
fontSize: '$1',
'@bp2': { fontSize: '$3' }
}}
>
<FooterLink
href="https://github.com/basementstudio/basement-grotesque/blob/master/LICENSE.txt"
target="_blank"
rel="noopener"
css={{
borderRight: '1px solid $colors$white'
}}
>
<Text className="label" as="span" uppercase heading>
EULA
</Text>
</FooterLink>
<FooterLink
target="_blank"
rel="noopener"
href="mailto:[email protected]"
>
<Text className="label" as="span" uppercase heading>
Contact
</Text>
</FooterLink>
</Box>
<Box
className="legal"
css={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Text css={{ fontSize: '$1', '@bp2': { fontSize: '$3' } }}>
© basement.studio LLC 2021 all rights reserved
</Text>
</Box>
</FooterGrid>
</Container>
</Section>
)
}
Example #16
Source File: VideoView.tsx From atlas with GNU General Public License v3.0 | 4 votes |
VideoView: React.FC = () => {
useRedirectMigratedContent({ type: 'video' })
const { id } = useParams()
const { openNftPutOnSale, cancelNftSale, openNftAcceptBid, openNftChangePrice, openNftPurchase, openNftSettlement } =
useNftActions()
const { withdrawBid } = useNftTransactions()
const { loading, video, error } = useVideo(id ?? '', {
onError: (error) => SentryLogger.error('Failed to load video data', 'VideoView', error),
})
const nftWidgetProps = useNftWidget(id)
const mdMatch = useMediaMatch('md')
const { addVideoView } = useAddVideoView()
const {
watchedVideos,
cinematicView,
actions: { updateWatchedVideos },
} = usePersonalDataStore((state) => state)
const category = useCategoryMatch(video?.category?.id)
const { anyOverlaysOpen } = useOverlayManager()
const { ref: playerRef, inView: isPlayerInView } = useInView()
const pausePlayNext = anyOverlaysOpen || !isPlayerInView
const { url: mediaUrl, isLoadingAsset: isMediaLoading } = useAsset(video?.media)
const { url: thumbnailUrl } = useAsset(video?.thumbnailPhoto)
const videoMetaTags = useMemo(() => {
if (!video || !thumbnailUrl) return {}
return generateVideoMetaTags(video, thumbnailUrl)
}, [video, thumbnailUrl])
const headTags = useHeadTags(video?.title, videoMetaTags)
const { startTimestamp, setStartTimestamp } = useVideoStartTimestamp(video?.duration)
// Restore an interrupted video state
useEffect(() => {
if (startTimestamp != null || !video) {
return
}
const currentVideo = watchedVideos.find((v) => v.id === video?.id)
setStartTimestamp(currentVideo?.__typename === 'INTERRUPTED' ? currentVideo.timestamp : 0)
}, [watchedVideos, startTimestamp, video, setStartTimestamp])
const channelId = video?.channel?.id
const channelName = video?.channel?.title
const videoId = video?.id
const categoryId = video?.category?.id
useEffect(() => {
if (!videoId || !channelId) {
return
}
addVideoView({
variables: {
videoId,
channelId,
categoryId,
},
}).catch((error) => {
SentryLogger.error('Failed to increase video views', 'VideoView', error)
})
}, [addVideoView, videoId, channelId, categoryId])
// Save the video timestamp
// disabling eslint for this line since debounce is an external fn and eslint can't figure out its args, so it will complain.
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleTimeUpdate = useCallback(
throttle((time) => {
if (video?.id) {
updateWatchedVideos('INTERRUPTED', video.id, time)
}
}, 5000),
[video?.id]
)
const handleVideoEnd = useCallback(() => {
if (video?.id) {
handleTimeUpdate.cancel()
updateWatchedVideos('COMPLETED', video?.id)
}
}, [video?.id, handleTimeUpdate, updateWatchedVideos])
// use Media Session API to provide rich metadata to the browser
useEffect(() => {
const supported = 'mediaSession' in navigator
if (!supported || !video) {
return
}
const artwork: MediaImage[] = thumbnailUrl ? [{ src: thumbnailUrl, type: 'image/webp', sizes: '640x360' }] : []
navigator.mediaSession.metadata = new MediaMetadata({
title: video.title || '',
artist: video.channel.title || '',
album: '',
artwork: artwork,
})
return () => {
navigator.mediaSession.metadata = null
}
}, [thumbnailUrl, video])
if (error) {
return <ViewErrorFallback />
}
if (!loading && !video) {
return (
<NotFoundVideoContainer>
<EmptyFallback
title="Video not found"
button={
<Button variant="secondary" size="large" to={absoluteRoutes.viewer.index()}>
Go back to home page
</Button>
}
/>
</NotFoundVideoContainer>
)
}
const isCinematic = cinematicView || !mdMatch
const sideItems = (
<GridItem colSpan={{ xxs: 12, md: 4 }}>
{!!nftWidgetProps && (
<NftWidget
{...nftWidgetProps}
onNftPutOnSale={() => id && openNftPutOnSale(id)}
onNftCancelSale={() => id && nftWidgetProps.saleType && cancelNftSale(id, nftWidgetProps.saleType)}
onNftAcceptBid={() => id && openNftAcceptBid(id)}
onNftChangePrice={() => id && openNftChangePrice(id)}
onNftPurchase={() => id && openNftPurchase(id)}
onNftSettlement={() => id && openNftSettlement(id)}
onNftBuyNow={() => id && openNftPurchase(id, { fixedPrice: true })}
onWithdrawBid={() => id && withdrawBid(id)}
/>
)}
<MoreVideos channelId={channelId} channelName={channelName} videoId={id} type="channel" />
<MoreVideos categoryId={category?.id} categoryName={category?.name} videoId={id} type="category" />
</GridItem>
)
const detailsItems = (
<>
{headTags}
<TitleContainer>
{video ? (
<TitleText variant={mdMatch ? 'h600' : 'h400'}>{video.title}</TitleText>
) : (
<SkeletonLoader height={mdMatch ? 56 : 32} width={400} />
)}
<Meta variant={mdMatch ? 't300' : 't100'} secondary>
{video ? (
formatVideoViewsAndDate(video.views || null, video.createdAt, { fullViews: true })
) : (
<SkeletonLoader height={24} width={200} />
)}
</Meta>
</TitleContainer>
<ChannelContainer>
<ChannelLink followButton id={channelId} textVariant="h300" avatarSize="small" />
</ChannelContainer>
<VideoDetails video={video} category={category} />
</>
)
return (
<>
<PlayerGridWrapper cinematicView={isCinematic}>
<PlayerWrapper cinematicView={isCinematic}>
<PlayerGridItem colSpan={{ xxs: 12, md: cinematicView ? 12 : 8 }}>
<PlayerContainer className={transitions.names.slide} cinematicView={cinematicView}>
{!isMediaLoading && video ? (
<VideoPlayer
isVideoPending={!video?.media?.isAccepted}
channelId={video?.channel?.id}
videoId={video?.id}
autoplay
src={mediaUrl}
onEnd={handleVideoEnd}
onTimeUpdated={handleTimeUpdate}
startTime={startTimestamp}
isPlayNextDisabled={pausePlayNext}
ref={playerRef}
/>
) : (
<PlayerSkeletonLoader />
)}
</PlayerContainer>
{!isCinematic && detailsItems}
</PlayerGridItem>
{!isCinematic && sideItems}
</PlayerWrapper>
</PlayerGridWrapper>
<LimitedWidthContainer>
{isCinematic && (
<LayoutGrid>
<GridItem className={transitions.names.slide} colSpan={{ xxs: 12, md: cinematicView ? 8 : 12 }}>
{detailsItems}
</GridItem>
{sideItems}
</LayoutGrid>
)}
<StyledCallToActionWrapper>
{['popular', 'new', 'discover'].map((item, idx) => (
<CallToActionButton key={`cta-${idx}`} {...CTA_MAP[item]} />
))}
</StyledCallToActionWrapper>
</LimitedWidthContainer>
</>
)
}
Example #17
Source File: HtmlLanding.tsx From frontend.ro with MIT License | 4 votes |
HtmlLanding = ({ isLoggedIn }: ConnectedProps<typeof connector>) => {
const router = useRouter();
const [showHtmlCssJs, setShowHtmlCssJs] = useState(false);
const chipRows = [
['<html>', '<div>', '<form>', '<head>', '<span>', '<article>', '<video>', '<button>', '<title>', '<main>', '<label>', '<summary>'],
['<aside>', '<pre>', '<code>', '<em>', '<br>', '<body>', '<header>', '<section>', '<p>', '<nav>', '<tbody>', '<progress>', '<h1>'],
['<blockquote>', '<ol>', '<footer>', '<audio>', '<img>', '<picture>', '<h2>', '<canvas>', '<figure>', '<hr>', '<ul>', '<select>'],
['<a>', '<time>', '<h3>', '<track>', '<iframe>', '<svg>', '<script>', '<link>', '<table>', '<input>', '<textarea>', '<details>'],
];
const { ref, inView } = useInView({
threshold: 1,
triggerOnce: true,
});
const startTutorial = () => {
// Temporary redirect to the lessons page,
// until we finish implementing the HTML Tutorial page.
router.push('/lectii');
};
const navigateToFirstSection = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
document.querySelector('#what-is-html').scrollIntoView();
};
const navigateToLogin = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
document.querySelector('#auth').scrollIntoView();
};
useEffect(() => {
if (inView) {
setShowHtmlCssJs(true);
}
}, [inView]);
withSmoothScroll();
return (
<>
<Header theme="black" withNavMenu />
<main data-certification-page className={styles.HtmlLanding}>
<div className={`${styles.hero} d-flex align-items-center justify-content-center bg-black text-white`}>
<h1 className="text-center mb-0"> Învață HTML de la zero </h1>
<p>
printr-un curs online, focusat pe
{' '}
<span className={styles['text-chip']}>
practică și feedback
</span>
{' '}
de la developeri cu experiență
</p>
<div className={`${styles['hero-controls']} d-flex align-items-center justify-content-between`}>
<a onClick={navigateToFirstSection} href="#what-is-html" className="btn btn--light no-underline">
Află mai multe
</a>
{isLoggedIn ? (
<Button variant="blue" onClick={startTutorial}>
Începe acum
</Button>
) : (
<a onClick={navigateToLogin} href="#auth" className="btn btn--blue no-underline">
Începe acum
</a>
)}
</div>
<div className={styles['hero-video']} />
</div>
<div id="what-is-html">
<div className={styles.section}>
<h2 className={styles['section-heading']}>
Ce este HTML-ul?
</h2>
<p className={styles['section-text']}>
Este punctul de start în călătoria fiecărui
{' '}
<a href="/intro/ce-este-frontend-ul">FrontEnd developer</a>
.
</p>
<p className={styles['section-text']}>
Alături de
{' '}
<span className="text-bold"> CSS </span>
{' '}
și
{' '}
<span className="text-bold"> JavaScript </span>
{' '}
este unul din cele 3 limbaje pe care
trebuie să le înveți pentru a construi site-uri și aplicații web.
</p>
</div>
<div ref={ref} />
<div className={styles['HtmlCssJs-wrapper']}>
{showHtmlCssJs && <HtmlCssJs />}
</div>
</div>
<div>
<section className={styles.section}>
<h2 className={styles['section-heading']}>
Ce vei învăța?
</h2>
<p className={styles['section-text']}>
Cursul acesta e destinat persoanelor cu zero (sau foarte puțină)
experiență în FrontEnd, deci vom începe de la lucrurile de bază
și apoi continua cu multă practică.
</p>
<List className={`${styles['learn-list']} ${styles['section-text']}`} variant="checkmark">
<li className="mt-8">
Ce e FrontEnd development-ul?
</li>
<li className="mt-4">
Care e rolul HTML-ului în cadrul ramurii de FrontEnd?
</li>
<li className="mt-4">
Cum să folosești
{' '}
<a href="https://code.visualstudio.com/" target="_blank" rel="noreferrer">
VSCode
</a>
{' '}
ca și editor de cod
</li>
<li className="mt-4">
Care sunt și cum să folosești cele mai importante elemente HTML
</li>
<li className="mt-4">
Bune practici în zona de scriere a codului, accesibilitate și perfomanță
</li>
</List>
</section>
<ChipCarousel className="my-10" rows={chipRows} />
</div>
<div>
<div className={styles.section}>
<h2 className={styles['section-heading']}>
Cum funcționează cursul?
</h2>
</div>
<HtmlHowItWorks className={styles.HtmlHowItWorks} />
</div>
<div>
<div className={styles.section}>
<h2 className={styles['section-heading']}>
Iar la final primești o certificare!
</h2>
<p className={styles['section-text']}>
În programare nu contează prea mult diplomele în sine, ci ce știi să faci.
De aceea, folosește această certificare ca o dovadă că ai reușit să scrii cod
real, gata de producție, validat de developeri cu experiență!
</p>
</div>
<div className={styles.section}>
<HtmlFakeDiploma />
</div>
</div>
<div id="auth">
<div className={styles.section}>
<h2 className={styles['section-heading']}>
Gata să începem?
</h2>
{isLoggedIn && (
<p className={styles['section-text']}>
Ești deja autentificat, deci apasă pe butonul de mai jos și hai să trecem la treabă!
</p>
)}
</div>
{isLoggedIn ? (
<div className="text-center text-2xl">
<Button onClick={startTutorial} variant="blue">
Începe acum
</Button>
</div>
) : (
<div className={styles['login-wrapper']}>
<Login
mode="register"
className={styles.login}
onSuccess={startTutorial}
/>
</div>
)}
</div>
</main>
</>
);
}