Bad rendering result of Safari when components including blur & gradient styles - javascript

I'm trying to do someting like this:
It's a glowing spot follows the pointer
import { useEffect, useState } from "react";
import "./styles.css";
const CUBE_SIZE = 100;
export default function App() {
const [left, setLeft] = useState(window.innerWidth / 2);
const [top, setTop] = useState(window.innerHeight / 2);
const mouseMoveHandler = (event) => {
setLeft(`${event.clientX - CUBE_SIZE / 2}px`);
setTop(`${event.clientY - CUBE_SIZE / 2}px`);
};
useEffect(() => {
document.addEventListener("mousemove", mouseMoveHandler);
return () => {
document.removeEventListener("mousemove", mouseMoveHandler);
};
}, []);
return (
<div className="App">
<div
style={{ width: `${CUBE_SIZE}px`, height: `${CUBE_SIZE}px`, left, top }}
className="glowing-cube"
/>
</div>
);
}
body {
background-color: black;
}
.App {
font-family: sans-serif;
text-align: center;
}
.glowing-cube {
position: relative;
background: linear-gradient(216.89deg, #dc4b5c 6.73%, #5a53ff 81.32%);
filter: blur(32px);
border-radius: 31px;
transform: rotate(135deg);
}
The codesandbox link is below:
https://7h7cpj.csb.app/
Everything looks perfect in Chrome, while it looks bad on Safari:
I found blur is supported since Safari 6 ref
Then what's the problem?
And, how can I do to improve this?
Is there a feasible solution on Safari?

Related

eventListener "animationend " behave unpredictably, or maybe it's because of setState function

I'm trying to make a toast message component that works like this.
It comes in from outside the right screen, disappears out of the screen after a certain period of time, and the component get deleted after the animation.
It works perfectly when I make a single component. However, when I create multiple toast components, some of them just get removed before the animation starts.
I tried many ways for 3days, but nothing worked... 😭 I would appreciate it if you could check which part is wrong. Help me plz
Toast Contatiner
import React, { useState, useCallback } from 'react';
import ToastItem from './Toast';
import styled from 'styled-components';
type ToastItemInfo = {
content: string | React.ReactNode;
id: string;
};
const ToastContainer = () => {
const [toastItems, setToastItems] = useState<ToastItemInfo[]>([]);
const handleClick = () => {
setToastItems((toastItems) => [
{
content: 'Toasted !',
id: String(Math.random()).substring(2, 8),
},
...toastItems,
]);
};
const removeToast = useCallback((id: string) => {
setToastItems((toastItems) => toastItems.filter((toast) => toast.id !== id));
}, []);
const hasItems = (toastItems: ToastItemInfo[]) => {
return toastItems.length > 0;
};
const mapItems = (toastItems: ToastItemInfo[]) => {
return toastItems.map((toast) => (
<ToastItem key={toast.id} id={toast.id} removeToast={removeToast}>
{toast.content}
</ToastItem>
));
};
return (
<div>
<Button onClick={handleClick}>Toast 🍞</Button>
{hasItems(toastItems) && <Container>{mapItems(toastItems)}</Container>}
</div>
);
};
const Container = styled.div`
position: fixed;
display: flex;
flex-direction: column;
gap: 8px;
top: 0;
right: 0;
margin: 16px;
`;
// .slideout {
// animation: 0.5s linear animate;
// }
// #keyframes animate {
// from {
// transform: translateX(0);
// }
// to {
// transform: translateX(120%);
// }
// }
const Button = styled.button`
width: 132px;
height: 40px;
background: #434253;
color: white;
font-size: 1rem;
border: 2px solid white;
cursor: pointer;
`;
export default ToastContainer;
ToastItem
import React, { memo, useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import ProgressBar from './ProgressBar';
interface ToastProps {
id: string;
children: string | React.ReactNode;
removeToast: (id: string) => void;
}
const autoCloseDelay = 3000;
const ToastItem: React.FC<ToastProps> = (props) => {
const { id, removeToast, children } = props;
const [toClose, setToClose] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const removeFn = () => {
removeToast(id);
};
const { current } = ref;
const timeout = setTimeout(() => {
current?.addEventListener('animationend', () => {
removeToast(id);
});
setToClose(true);
}, autoCloseDelay);
return () => {
clearTimeout(timeout);
current?.removeEventListener('animationend', removeFn);
};
}, [id, removeToast]);
return (
<Toast id={id} ref={ref} onClick={() => removeToast(id)} toClose={toClose}>
{children}
<ProgressBar autoCloseDelay={autoCloseDelay} />
</Toast>
);
};
const slideIn = keyframes`
from{
transform: translateX(120%);
}
to{
transform: translateX(0);
}
`;
const slideOut = keyframes`
from{
transform: translateX(0);
}
to{
transform: translateX(120%);
}
`;
const Toast = styled.div<{ toClose: boolean }>`
box-sizing: border-box;
position: relative;
padding: 0 16px;
width: 250px;
height: 58px;
line-height: 58px;
background: #fabb4d;
color: #5f2700;
border-radius: 2px;
cursor: pointer;
animation: 0.5s ease 0s ${(props) => (props.toClose ? slideOut : slideIn)};
&:hover {
transform: scale(1.05);
}
`;
export default memo(ToastItem);
enter image description here

React: spinner effect is canceling my scrolling effect

OK guys, so I'm noob in React, and I know that I'm mistaken something, but I can't see what.
I put loading spinners between my pages and no problems, until this page :
So I have this page that is scrolling on X axe and a logo spinning during scroll, until know, all was working correctly. And there is the code without the loading spinner :
import React, { useEffect, useRef } from "react";
import styled, { ThemeProvider } from "styled-components";
import { DarkTheme } from "./Themes";
import {motion} from 'framer-motion';
import LogoComponent from '../subComponents/LogoComponent';
import SocialIcons from '../subComponents/SocialIcons';
import PowerButton from '../subComponents/PowerButton';
import { Work } from '../data/WorkData';
import Card from "../subComponents/Card";
import { Umbrella } from "./AllSvgs";
import BigTitle from "../subComponents/BigTitle";
const Box = styled.div`
background-color: ${props => props.theme.body};
height: 400vh;
position: relative;
display: flex;
align-items: center;
`
const Main = styled(motion.ul)`
position: fixed;
top: 12rem;
left: calc(10rem + 15vw);
height: 40vh;
display: flex;
color: white;
`
const Rotate = styled.span`
display: block;
position: fixed;
right: 1rem;
bottom: 1rem;
width: 80px;
height: 80px;
z-index:1;
`
// Framer-motion configuration
const container = {
hidden: {opacity:0},
show: {
opacity:1,
transition: {
staggerChildren:0.5,
duration:0.5,
}
}
}
const WorkPage = () => {
const ref = useRef(null);
const umbrella = useRef(null);
useEffect(() => {
let element = ref.current;
const rotate = () => {
element.style.transform = `translateX(${-window.scrollY}px)`
umbrella.current.style.transform = `rotate(` + -window.scrollY + 'deg)'
}
window.addEventListener('scroll', rotate)
return () => window.removeEventListener('scroll', rotate)
}, [])
return (
<ThemeProvider theme={DarkTheme}>
<Box>
<LogoComponent theme='dark'/>
<SocialIcons theme='dark'/>
<PowerButton />
<Main ref={ref} variants={container} initial='hidden' animate='show' >
{
Work.map( d =>
<Card key={d.id} data={d} />
)
}
</Main>
<Rotate ref={umbrella}>
<Umbrella width={80} height={80} fill={DarkTheme.theme} />
</Rotate>
<BigTitle text="PROJETS" top="10%" right="20%" />
</Box>
</ThemeProvider>
)
}
export default WorkPage
Then I put the code with the same logic than the other pages that are working :
import React, { useEffect, useRef, useState } from "react";
import styled, { ThemeProvider } from "styled-components";
import { DarkTheme } from "./Themes";
import {motion} from 'framer-motion';
import RingLoader from "react-spinners/RingLoader";
import { css } from "#emotion/react";
import LogoComponent from '../subComponents/LogoComponent';
import SocialIcons from '../subComponents/SocialIcons';
import PowerButton from '../subComponents/PowerButton';
import { Work } from '../data/WorkData';
import Card from "../subComponents/Card";
import { Umbrella } from "./AllSvgs";
import BigTitle from "../subComponents/BigTitle";
const Box = styled.div`
background-color: ${props => props.theme.body};
height: 400vh;
position: relative;
display: flex;
align-items: center;
`
const Main = styled(motion.ul)`
position: fixed;
top: 12rem;
left: calc(10rem + 15vw);
height: 40vh;
display: flex;
color: white;
`
const Rotate = styled.span`
display: block;
position: fixed;
right: 1rem;
bottom: 1rem;
width: 80px;
height: 80px;
z-index:1;
`
const override = css`
position: absolute;
bottom: 10%;
right: 10%;
`
// Framer-motion configuration
const container = {
hidden: {opacity:0},
show: {
opacity:1,
transition: {
staggerChildren:0.5,
duration:0.5,
}
}
}
const WorkPage = () => {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true)
setTimeout(() => {
setLoading(false)
}, 2000)
}, [])
const ref = useRef(null);
const umbrella = useRef(null);
useEffect(() => {
let element = ref.current;
const rotate = () => {
element.style.transform = `translateX(${-window.scrollY}px)`
umbrella.current.style.transform = `rotate(` + -window.scrollY + 'deg)'
}
window.addEventListener('scroll', rotate)
return () => window.removeEventListener('scroll', rotate)
}, [])
return (
<ThemeProvider theme={DarkTheme}>
{
loading ?
<RingLoader
color={'#000'}
loading={loading}
size={60}
css={override}
/>
:
<Box>
<LogoComponent theme='dark'/>
<SocialIcons theme='dark'/>
<PowerButton />
<Main ref={ref} variants={container} initial='hidden' animate='show' >
{
Work.map( d =>
<Card key={d.id} data={d} />
)
}
</Main>
<Rotate ref={umbrella}>
<Umbrella width={80} height={80} fill={DarkTheme.theme} />
</Rotate>
<BigTitle text="PROJETS" top="10%" right="20%" />
</Box>
}
</ThemeProvider>
)
}
export default WorkPage
And the scroll is not working anymore. The logo is still spinning. I try to put in conditions, but nope.
Put the useState on true, but then I have an error on the rotate func :
TypeError: Cannot read properties of null (reading 'style')
rotate
src/components/WorkPage.js:89
86 | let element = ref.current;
87 |
88 | const rotate = () => {
> 89 | element.style.transform = `translateX(${-window.scrollY}px)`
| ^ 90 |
91 | umbrella.current.style.transform = `rotate(` + -window.scrollY + 'deg)'
92 | }
I don't see what I'm missing... Thanks y'all 🙏
In your second example code (where you added loading and setLoading from the useState hook), the <Box> component (which contains the <Main ref={ref}... component) doesn't get rendered when loading is set to true (and you're setting it to true in your first useEffect).
Since the <Main ref={ref}... component isn't being rendered, the line let element = ref.current will initialize element as null. (that's why you're getting that error)

how to Show or hide Navbar when scroll use react.js?

I used react.js Hooks with useState and useEffect, when I scroll-down and the screen comes down Header hides after 250 pixels. Now I want to know how to display Header using the react Hooks when I scroll up.
const Navbar = () => {
const [show, setShow] = useState(false)
const controlNavbar = () => {
if (window.scrollY > 250 ) {
setShow(true)
}else{
setShow(false)
}
}
useEffect(() => {
window.addEventListener('scroll', controlNavbar)
return () => {
window.removeEventListener('scroll', controlNavbar)
}
}, [])
and header:
<header className={`active ${show && 'hidden'}`}></header>
css:
.active{
height: 4rem;
width: 100%;
position: fixed;
top: 0px;
transition: 0.3s linear;
display: flex;
justify-content:stretch;
align-items: center;
background-color: #FFFFFF;
border-bottom: 1px solid rgba(0, 0, 0, .1);
z-index: 40;
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, .08);
/* padding: 0 7%; */
}
.hidden{
height: 4rem;
width: 100%;
z-index: 40;
border-bottom: 1px solid rgba(0, 0, 0, .1);
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, .08);
position: fixed;
top: -80px;
transition: 0.3s linear;
}
Instead of using a static value (250), you need to perform dynamic checking with the previous scroll. This is my complete solution (using nextJS):
import React, { useState, useEffect } from 'react';
const Navbar = () => {
const [show, setShow] = useState(true);
const [lastScrollY, setLastScrollY] = useState(0);
const controlNavbar = () => {
if (typeof window !== 'undefined') {
if (window.scrollY > lastScrollY) { // if scroll down hide the navbar
setShow(false);
} else { // if scroll up show the navbar
setShow(true);
}
// remember current page location to use in the next move
setLastScrollY(window.scrollY);
}
};
useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('scroll', controlNavbar);
// cleanup function
return () => {
window.removeEventListener('scroll', controlNavbar);
};
}
}, [lastScrollY]);
return (
<nav className={`active ${show && 'hidden'}`}>
....
</nav>
);
};
export default Navbar;
Simplest answer using tailwindCSS
import React, {useState, useEffect} from 'react'
const Navbar = () => {
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true)
const handleScroll = () => {
const currentScrollPos = window.scrollY
if(currentScrollPos > prevScrollPos){
setVisible(false)
}else{
setVisible(true)
}
setPrevScrollPos(currentScrollPos)
}
useEffect( () => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll)
})
return (
<div className={`bg-slate-700 h-14 sticky ${visible ? 'top-0' : ''} `}>
Some Company Name
</div>
)
}
export default Navbar
I have come up with better approach which is much optimized.
useEffect(() => {
let previousScrollPosition = 0;
let currentScrollPosition = 0;
window.addEventListener('scroll', function (e) {
// Get the new Value
currentScrollPosition = window.pageYOffset;
//Subtract the two and conclude
if (previousScrollPosition - currentScrollPosition < 0) {
setShow(false);
} else if (previousScrollPosition - currentScrollPosition > 0) {
setShow(true);
}
// Update the previous value
previousScrollPosition = currentScrollPosition;
});
}, []);
From a couple of searches here I stumbled upon a scroll funtion on raw js but you can implement it in your case and tweak it how you want it, because currently your function will only use the false if scrollY < 250...
On your controlNav function create a variable that will track the location/point of your previous scroll then compare it to the current value of the scroll.. it will look like this:
const controlNavbar = () => {
if (window.scrollY >= this.lastScroll ) {
setShow(true)
}else{
setShow(false)
}
this.lastScroll = window.scrollY;
}
NB This will only work for scroll up and scroll down with no minimum value..
reference to the raw js

How to switch image on scroll in React (like on Apple website)?

I try to switch from an image to another one when scrolling with React but I don't know how to do this... maybe with React hook. I know it's possible with jQuery but I don't want to use it.
Example here: https://www.apple.com/iphone-12/, look at the "Five fresh finishes".
import React, {useState} from 'react';
import './BackgroundDiscover.css';
import logoOrdiW from "../../images/mathis_computer_white.jpg";
import logoOrdiB from "../../images/mathis_computer_black.jpg";
const BackgroundDiscover = () => {
const [imageSrc, SetSrc] = useState(logoOrdiW);
const switchBrackground = () => {
SetSrc(logoOrdiB);
}
return (
<>
<div className="container-discover">
<div className='container-logo-white'>
<img src={imageSrc} alt="logo ordinateur mathis blanc" onScroll={switchBrackground}/>
</div>
</div>
</>
);
};
export default BackgroundDiscover;
.container-discover {
display: flex;
flex-direction: column;
}
.container-logo-white img {
width: 100%;
}
.container-logo-black img {
width: 100%;
}
You don't need jQuery or something like this. You could subscribe to scroll event in useEffect() hook with addEventListener and check scrollTop of a scrolling container.
If you want to do the same effect as the Apple website has, you could add some magic with position: sticky, height in vh units and checking window.innerHeight.
Note that this code is simplified, not optimized, and only needed to understand the idea:
CodeSandbox
index.js:
import { useLayoutEffect, useState } from "react";
import { render } from "react-dom";
import classnames from "classnames";
import "./index.css";
const images = [0, 1, 2, 3, 4];
const App = () => {
const [visibleImagesMap, setVisibleImagesMap] = useState(
images.reduce((map, image) => {
map[image] = false;
return map;
}, {})
);
useLayoutEffect(() => {
const handleScroll = () => {
const scrollTop = document.documentElement.scrollTop;
const viewportHeight = window.innerHeight;
const newVisibleImagesMap = images.reduce((map, image) => {
map[image] = scrollTop >= image * viewportHeight;
return map;
}, {});
setVisibleImagesMap(newVisibleImagesMap);
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="app">
<div className="sticky">
<div className="frame">
{images.map((image) => (
<div
className={classnames("image", `image_${image}`, {
image_visible: visibleImagesMap[image]
})}
key={image}
/>
))}
</div>
</div>
</div>
);
};
render(<App />, document.getElementById("root"));
index.css:
body {
margin: 0;
}
.app {
height: 500vh;
}
.sticky {
position: sticky;
top: 0;
height: 100vh;
}
.frame {
z-index: 1;
position: relative;
height: 100%;
width: 100%;
}
.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
background-repeat: no-repeat;
background-size: cover;
}
.image_0 {
z-index: 0;
background-image: url("./images/0.jpeg");
}
.image_1 {
z-index: 1;
background-image: url("./images/1.jpeg");
}
.image_2 {
z-index: 2;
background-image: url("./images/2.jpeg");
}
.image_3 {
z-index: 3;
background-image: url("./images/3.jpeg");
}
.image_4 {
z-index: 4;
background-image: url("./images/4.jpeg");
}
.image_visible {
opacity: 1;
}
There are plenty of solutions for this but basically you can set in this way.
Wheel event listener turns either positive or negative value so you should create an algorithm if you want to impelement different designs.
import React, { useState } from "react";
export default function App() {
const [bgImage, handleImage] = useState(
"https://media.wired.com/photos/5e9f56f143e5800008514457/1:1/w_1277,h_1277,c_limit/Gear-Feature-Apple_new-iphone-se-white_04152020.jpg"
);
window.addEventListener("wheel", (e) => {
if (e.deltaY > 0) {
handleImage(
"https://media.wired.com/photos/5bcea2642eea7906bba84c67/master/w_2560%2Cc_limit/iphonexr.jpg"
);
} else {
handleImage(
"https://media.wired.com/photos/5e9f56f143e5800008514457/1:1/w_1277,h_1277,c_limit/Gear-Feature-Apple_new-iphone-se-white_04152020.jpg"
);
}
});
return (
<div className="App">
<img
style={{ width: "400px", height: "300px" }}
id="myimage"
src={bgImage}
alt=""
/>
</div>
);
}
And as a result you can use bgImage state in your image src.
This is a codesandbox example https://codesandbox.io/s/pensive-vaughan-cfc63?fontsize=14&hidenavigation=1&theme=dark
You can try useEffect and useRef.
useEffect is sideload functions, and useRef is for real dom referances.
import React, { useRef, useState, useEffect } from 'react';
import logoOrdiW from "../../images/mathis_computer_white.jpg";
import logoOrdiB from "../../images/mathis_computer_black.jpg";
import './BackgroundDiscover.css';
const BackgroundDiscover = () => {
const img = useRef();
const [src, setSrc] = useState(logoOrdiW);
const switchBrackground = () => {
setSrc(logoOrdiB);
}
useEffect(() => {
img.current.onscroll = switchBrackground;
}, []);
return (
<div className="container-discover">
<div className='container-logo-white'>
<img src={src} alt="logo ordinateur mathis blanc" ref={img} />
</div>
</div>
);
};
export default BackgroundDiscover;

Adding a loader to react component

I'm trying to implement a loader to my react component, when the background image is loading it should display 'loading' and then once it has loaded it should display 'loaded'
I have a setTimeout() on my componentwillMount() to test that the loader functions as expected which it does
I'm struggling to understand how it knows when the image is loaded and to change the loading state
Is it best to put the image into a seperate component with the loader rather than have it on the Hello component?
https://www.webpackbin.com/bins/-KsOEkf9ubvR6iz4bMxG
Update
I have managed to get a simple image loader working using the onload() method attached to the image - https://www.webpackbin.com/bins/-KsNpZPzveUoby1yriFo
Hello.js
import React from 'react'
import Loader from './Loader'
import styled from 'styled-components'
const Card = styled.div`
height: 400px;
width:20%;
background: url('https://www.planwallpaper.com/static/images/Light-Wood-Background-Wallpaper.jpg');
`
export default class Test extends React.Component {
constructor() {
super()
this.state = { loading:true}
}
componentWillMount()
{
setTimeout(() => this.setState({loading: false}), 3000)
console.log("componentDidMount");
}
render() {
return (
<div>
<Card>
<Loader loading={this.state.loading} />
</Card>
</div>
)
}
}
Loader.js
import React, { Component } from 'react'
import styled, { keyframes } from 'styled-components'
import { string } from 'prop-types'
const transition1 = keyframes`
0% { background: #F19939; }
33.33% { background: #F8CA8F; }
66.66% { background: #FBD8AE; }
100% { background: #F19939; }
`
const transition2 = keyframes`
0% { background: #FBD8AE; }
33.33% { background: #F19939; }
66.66% { background: #F8CA8F; }
100% { background: #FBD8AE; }
`
const transition3 = keyframes`
0% { background: #F8CA8F; }
33.33% { background: #FBD8AE; }
66.66% { background: #F19939; }
100% { background: #F8CA8F; }
`
const Box = styled.span`
height: 12px;
width: 12px;
margin: 0 3px 0 3px;
border-radius: 4px;
animation: 0.4s ${transition1 } infinite;
`
const Box2 = styled(Box)`
animation: 0.4s ${transition2 } infinite;
`
const Box3 = styled(Box)`
animation: 0.4s ${transition3 } infinite;
`
const TextWrap = styled.div`
display: flex;
flex: 0 0 100%;
justify-content: center;
color: #fff;
`
const Para = styled.p`
color: #fff
padding: 19px 0 0 0;
`
const ParaLoaded = styled(Para)`
color: #fff;
padding: 22px 0 0 0;
`
export default class Loader extends Component {
render() {
return (
<div >
<div >
<TextWrap>
{
this.props.loading
?
<Para>Loading...</Para>
:
<ParaLoaded>Loaded</ParaLoaded>
}
</TextWrap>
</div>
</div>
)
}
}
You can do it like this: https://www.webpackbin.com/bins/-KsOJpBVfpazfXghJAaF
LoadBackgroundImage.js
const LoadBackgroundImage = (component, imageUrl, seconds, success, failure) => {
let timeoutOccured = false;
const image = new Image();
const timeout = setTimeout(() => {
timeoutOccured = true;
failure();
}, seconds * 1000);
image.onload = () => {
clearTimeout(timeout);
component.style.backgroundImage = `url('${imageUrl}')`;
if (!timeoutOccured) success();
};
image.src = imageUrl;
};
export default LoadBackgroundImage;
Hello.js
import React from 'react'
import Loader from './Loader'
import styled from 'styled-components'
import LoadBackgroundImage from './LoadBackgroundImage'
const Card = styled.div`
height: 400px;
width:20%;
`
export default class Hello extends React.Component {
constructor() {
super()
this.state = { loading:true}
}
componentDidMount() {
LoadBackgroundImage(
this.card,
'https://www.planwallpaper.com/static/images/Light-Wood-Background-Wallpaper.jpg',
5,
() => this.setState({ loading: false }),
() => console.log('The image did not load in 5 seconds')
);
}
render() {
return (
<div>
<Card innerRef={card => this.card = card}>
<Loader loading={this.state.loading} />
</Card>
</div>
)
}
}
In render() you are using innerRef to obtain a reference to the card component and save it in this.card. Then in componentDidMount you are using this reference and LoadBackgroundImage function to load the image and monitor when it's loaded. If the image is loaded in given number of second success callback will be called, otherwise failure callback will be called. The image can still load after 5 seconds, but the success callback will not be called. If you want to be called anyway, you can skip this ckeck: if (!timeoutOccured) in LoadBackgroundImage function.

Categories

Resources