Skip to content

Commit

Permalink
Dynamically set columns count (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
MrBartusek authored May 15, 2023
1 parent b20dfbb commit 4bb6418
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 88 deletions.
88 changes: 44 additions & 44 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
module.exports = {
env: {
'browser': true,
'jest': true
},
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:storybook/recommended'],
overrides: [],
settings: {
react: {
version: 'detect'
}
},
ignorePatterns: ['*.html'],
parser: '@typescript-eslint/parser',
parserOptions: {
'ecmaVersion': '13',
'sourceType': 'module'
},
plugins: ['react', '@typescript-eslint'],
rules: {
'brace-style': ['error', 'stroustrup', {
'allowSingleLine': true
}],
'quotes': ['error', 'single'],
'no-trailing-spaces': ['error'],
'eol-last': ['error', 'always'],
'curly': ['error', 'multi-line', 'consistent'],
'indent': ['error', 'tab', {
'SwitchCase': 1,
'ignoredNodes': ['TemplateLiteral *']
}],
'semi': ['error', 'always'],
'no-multiple-empty-lines': ['error', {
'max': 1
}],
'comma-dangle': ['error', 'never'],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'array-bracket-spacing': ['error', 'always'],
'@typescript-eslint/no-use-before-define': 'off'
}
};
env: {
'browser': true,
'jest': true
},
extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:storybook/recommended' ],
overrides: [],
settings: {
react: {
version: 'detect'
}
},
ignorePatterns: [ '*.html' ],
parser: '@typescript-eslint/parser',
parserOptions: {
'ecmaVersion': '13',
'sourceType': 'module'
},
plugins: [ 'react', '@typescript-eslint' ],
rules: {
'brace-style': [ 'error', 'stroustrup', {
'allowSingleLine': true
} ],
'quotes': [ 'error', 'single' ],
'no-trailing-spaces': [ 'error' ],
'eol-last': [ 'error', 'always' ],
'curly': [ 'error', 'multi-line', 'consistent' ],
'indent': [ 'error', 'tab', {
'SwitchCase': 1,
'ignoredNodes': [ 'TemplateLiteral *' ]
} ],
'semi': [ 'error', 'always' ],
'no-multiple-empty-lines': [ 'error', {
'max': 1
} ],
'comma-dangle': [ 'error', 'never' ],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'array-bracket-spacing': [ 'error', 'always' ],
'@typescript-eslint/no-use-before-define': 'off'
}
};
2 changes: 1 addition & 1 deletion src/GifPickerReact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function GifPickerReact(props: GifPickerReactProps): JSX.Element {
<TenorContext.Provider value={tenorManager}>
<PickerMain>
<Header />
<Body />
<Body width={props.width} />
</PickerMain>
</TenorContext.Provider>
</PickerContext.Provider>
Expand Down
37 changes: 31 additions & 6 deletions src/components/body/Body.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import PickerContext from '../../context/PickerContext';
import TenorContext from '../../context/TenorContext';
import { TenorCategory } from '../../managers/TenorManager';
Expand All @@ -8,12 +8,27 @@ import CategoryList from './CategoryList';
import SearchResult from './SearchResult';
import TrendingResult from './TrendingResult';

function Body(): JSX.Element {
const MAX_COLUMN_WIDTH = 170;

export interface BodyProps {
/**
* Width prop is here is for sole purpose of updating the
* columns calculations when width prop change
*/
width?: number | string;
}

function Body({ width }: BodyProps): JSX.Element {
const [ categories, setCategories ] = useState<TenorCategory[] | undefined>(undefined);
const [ trending, setTrending ] = useState<TenorImage | undefined>(undefined);
const [ pickerContext ] = useContext(PickerContext);
const [ columnsCount, setColumnsCount ] = useState(1);
const tenor = useContext(TenorContext);
const ref = useRef<HTMLDivElement>(null);

/**
* Load categories and first trending image for home page
*/
useEffect(() => {
(async (): Promise<any> => {
const categoryList = await tenor.categories();
Expand All @@ -23,17 +38,27 @@ function Body(): JSX.Element {
})();
}, []);

/**
* Calculate amount of columns to display
*/
useEffect(() => {
const width = ref.current ? ref.current.offsetWidth : 0;
let columns = Math.floor( width / MAX_COLUMN_WIDTH );
if(columns < 1) columns = 1;
setColumnsCount(columns);
}, [ ref.current, width ]);

return (
<div className='gpr-body'>
<div className='gpr-body' ref={ref}>
{((): JSX.Element => {
if(pickerContext.showTrending) {
return <TrendingResult />;
return <TrendingResult columnsCount={columnsCount} />;
}
else if(pickerContext.searchTerm) {
return <SearchResult searchTerm={pickerContext.searchTerm} />;
return <SearchResult columnsCount={columnsCount} searchTerm={pickerContext.searchTerm} />;
}
else {
return <CategoryList categories={categories} trending={trending} />;
return <CategoryList columnsCount={columnsCount} categories={categories} trending={trending} />;
}
})()}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/body/Category.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}

.GifPickerReact .gpr-category-overlay .gpr-category-name {
Expand Down
1 change: 0 additions & 1 deletion src/components/body/CategoryList.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
display: grid;
grid-gap: var(--gpr-category-list-padding);
padding: var(--gpr-body-padding);
grid-template-columns: 1fr 1fr;
flex: 1;
grid-auto-rows: min-content;
overflow-y: scroll;
Expand Down
15 changes: 11 additions & 4 deletions src/components/body/CategoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,28 @@ import TrendingCategory from './TrendingCategory';
export interface CategoryListProps {
categories?: TenorCategory[];
trending?: TenorImage;
columnsCount: number;
}

function CategoryList({ categories, trending }: CategoryListProps): JSX.Element {
function CategoryList({ categories, trending, columnsCount }: CategoryListProps): JSX.Element {
// Reduce categories to multiple of columns so there won't by any not full rows at the bottom
let categoriesSliced = categories;
if(columnsCount > 1) {
categoriesSliced = categories?.slice(0, -((categories.length + 1) % columnsCount));
}

return (
<div className='gpr-category-list'>
<div className='gpr-category-list' style={{gridTemplateColumns: `repeat(${columnsCount}, 1fr)`}}>
{categories && trending ? (
<>
<TrendingCategory image={trending.url} />
{categories.map((cat, i) => (
{categoriesSliced?.map((cat, i) => (
<FeaturedCategory key={i} image={cat.image} name={cat.name} />
))}
</>
) : (
<>
{[ ...Array(10) ].map((_, i) => (
{[ ...Array(10 * columnsCount) ].map((_, i) => (
<CategoryPlaceholder key={i} />
))}
</>
Expand Down
34 changes: 19 additions & 15 deletions src/components/body/GifList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ export interface GifListProps {
isLoading: boolean;
result?: TenorResult;
searchTerm?: string;
columnsCount: number;
}

function GifList({ isLoading, result, searchTerm }: GifListProps): JSX.Element {
const columns = useMemo(() => calculateColumns(result), [ result ]);
function GifList({ isLoading, result, searchTerm, columnsCount }: GifListProps): JSX.Element {
const columns = useMemo(() => generateColumns(result, columnsCount), [ result, columnsCount ]);
const isEmpty = !result || result.images.length <= 0;

if(isLoading) {
return <GifListPlaceholder />;
return <GifListPlaceholder columnsCount={columnsCount} />;
}

if(isEmpty) {
Expand All @@ -40,22 +41,25 @@ function GifList({ isLoading, result, searchTerm }: GifListProps): JSX.Element {
);
}

// TODO: Support multiple columns
function calculateColumns(result?: TenorResult): TenorImage[][] {
/**
* Splits TenorResult into grid of TenorImage with set amount of columns
* Columns should have more or less similar height but don't necessarily need to
* have fixed amount of elements, GIFs don't have uniform heights
*
* @returns array of columns (which are the arrays of TenorImage)
*/
function generateColumns(result?: TenorResult, columnsCount = 2 ): TenorImage[][] {
if(!result) return [];
const columns: TenorImage[][] = [ [], [] ];
const columnHeight = [ 0,0 ];
const columns: TenorImage[][] = new Array(columnsCount).fill(null).map(() => []);
const columnsHeight = new Array(columnsCount).fill(0);

for(const img of result.images) {
const aspectRatio = img.preview.height / img.preview.width;
if(columnHeight[0] > columnHeight[1]) {
columns[1].push(img);
columnHeight[1] += aspectRatio;
}
else {
columns[0].push(img);
columnHeight[0] += aspectRatio;
}
// We want to put image of this loop in shortest column (smallest width)
const shortestColumnIndex = columnsHeight.indexOf(Math.min(...columnsHeight));
columns[shortestColumnIndex].push(img);
// Here we actually add aspect ratio rather than height since design is responsive
columnsHeight[shortestColumnIndex] += aspectRatio;
}
return columns;
}
Expand Down
34 changes: 22 additions & 12 deletions src/components/body/GifListPlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import React from 'react';
import ResultPlaceholder from '../placeholders/ResultPlaceholder';

const SHOW_DELAY = 70;
const PLACEHOLDER_HEIGHTS = [
[ 120, 100, 130, 175, 154, 110 ],
[ 150, 115, 135, 154, 145, 170 ],
[ 140, 125, 120, 150, 100, 125 ],
[ 130, 145, 175, 120, 135, 100 ]
];

function GifListPlaceholder() {
export interface GifListPlaceholderProps {
columnsCount: number;
showDelay?: number;
}

function GifListPlaceholder({ columnsCount, showDelay = 70 }: GifListPlaceholderProps) {
return (
<div className='gpr-gif-list'>
<div className='gpr-gif-list-column'>
{[ 120, 100, 130, 175, 154 ].map((height, i) => (
<ResultPlaceholder key={i} height={height} showDelay={((i + 1) * SHOW_DELAY * 2) - SHOW_DELAY} />
))}
</div>
<div className='gpr-gif-list-column'>
{[ 150, 115, 135, 154, 145 ].map((height, i) => (
<ResultPlaceholder key={i} height={height} showDelay={((i + 1) * SHOW_DELAY * 2)} />
))}
</div>
{[ ...Array(columnsCount) ].map((_, i) => (
<div className='gpr-gif-list-column' key={i}>
{PLACEHOLDER_HEIGHTS[i % PLACEHOLDER_HEIGHTS.length].map((height, j) => (
<ResultPlaceholder
key={j} height={height}
showDelay={((j + 1) * showDelay * columnsCount) + showDelay * i}
/>
))}
</div>
))}
</div>
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/body/SearchResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import GifList from './GifList';

export interface SearchResultProps {
searchTerm: string;
columnsCount: number;
}

function SearchResult({ searchTerm }: SearchResultProps) {
function SearchResult({ searchTerm, columnsCount }: SearchResultProps) {
const [ searchResult, setSearchResult ] = useState<TenorResult>(null!);
const [ isLoading, setLoading ] = useState(true);
const tenor = useContext(TenorContext);
Expand All @@ -24,7 +25,7 @@ function SearchResult({ searchTerm }: SearchResultProps) {
}, [ searchTerm ]);

return (
<GifList isLoading={isLoading} result={searchResult} searchTerm={searchTerm} />
<GifList isLoading={isLoading} columnsCount={columnsCount} result={searchResult} searchTerm={searchTerm} />
);
}

Expand Down
8 changes: 6 additions & 2 deletions src/components/body/TrendingResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import TenorContext from '../../context/TenorContext';
import { TenorResult } from '../../managers/TenorManager';
import GifList from './GifList';

function TrendingResult() {
export interface TrendingResultProps {
columnsCount: number;
}

function TrendingResult({ columnsCount }: TrendingResultProps) {
const [ trendingResult, setSearchResult ] = useState<TenorResult>(null!);
const [ isLoading, setLoading ] = useState(true);

Expand All @@ -20,7 +24,7 @@ function TrendingResult() {
}, [ ]);

return (
<GifList isLoading={isLoading} result={trendingResult} />
<GifList columnsCount={columnsCount} isLoading={isLoading} result={trendingResult} />
);
}

Expand Down
1 change: 0 additions & 1 deletion src/stories/GifPicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export const DarkTheme = {
export const Search = {
...Home,
play: async ({ canvasElement }: any) => {
console.log(canvasElement);
const canvas = within(canvasElement);

await userEvent.type(canvas.getByTestId('gpr-search-input'), 'patrick bateman');
Expand Down

0 comments on commit 4bb6418

Please sign in to comment.