Skip to content

Commit

Permalink
ntp: favorites ship review (#1432)
Browse files Browse the repository at this point in the history
* ntp: favorites ship review

* slightly larger hover

* don't allow animations on macos when it's a userImage background

* fixed logic

* linting
  • Loading branch information
shakyShane authored Jan 28, 2025
1 parent ad9bc97 commit 2145e03
Show file tree
Hide file tree
Showing 20 changed files with 648 additions and 418 deletions.
78 changes: 78 additions & 0 deletions special-pages/pages/new-tab/app/components/CompanyIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import styles from './CompanyIcon.module.css';
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../privacy-stats/constants.js';
import { h } from 'preact';
import { useState } from 'preact/hooks';

const mappings = {
'google-analytics-google': 'google-analytics',
};

const states = /** @type {const} */ ({
loading: 'loading',
loaded: 'loaded',
loadingFallback: 'loadingFallback',
loadedFallback: 'loadedFallback',
errored: 'errored',
});

/**
* @typedef {states[keyof states]} State
*/

/**
* @param {object} props
* @param {string} props.displayName
*/
export function CompanyIcon({ displayName }) {
const icon = displayName.toLowerCase().split('.')[0];
const cleaned = icon.replace(/[^a-z ]/g, '').replace(/ /g, '-');
const id = cleaned in mappings ? mappings[cleaned] : cleaned;
const firstChar = id[0];
const [state, setState] = useState(/** @type {State} */ (states.loading));

const src =
state === 'loading' || state === 'loaded'
? `./company-icons/${id}.svg`
: state === 'loadingFallback' || state === 'loadedFallback'
? `./company-icons/${firstChar}.svg`
: null;

if (src === null || icon === DDG_STATS_OTHER_COMPANY_IDENTIFIER) {
return (
<span className={styles.icon}>
<Other />
</span>
);
}

return (
<span className={styles.icon}>
<img
src={src}
alt={''}
class={styles.companyImgIcon}
data-loaded={state === states.loaded || state === states.loadedFallback}
onLoad={() => setState((prev) => (prev === states.loading ? states.loaded : states.loadedFallback))}
onError={() => {
setState((prev) => {
if (prev === states.loading) return states.loadingFallback;
return states.errored;
});
}}
/>
</span>
);
}

function Other() {
return (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 16C1 7.71573 7.71573 1 16 1C24.2843 1 31 7.71573 31 16C31 16.0648 30.9996 16.1295 30.9988 16.1941C30.9996 16.2126 31 16.2313 31 16.25C31 16.284 30.9986 16.3177 30.996 16.3511C30.8094 24.4732 24.1669 31 16 31C7.83308 31 1.19057 24.4732 1.00403 16.3511C1.00136 16.3177 1 16.284 1 16.25C1 16.2313 1.00041 16.2126 1.00123 16.1941C1.00041 16.1295 1 16.0648 1 16ZM3.58907 17.5C4.12835 22.0093 7.06824 25.781 11.0941 27.5006C10.8572 27.0971 10.6399 26.674 10.4426 26.24C9.37903 23.9001 8.69388 20.8489 8.53532 17.5H3.58907ZM8.51564 15H3.53942C3.91376 10.2707 6.92031 6.28219 11.0941 4.49944C10.8572 4.90292 10.6399 5.326 10.4426 5.76003C9.32633 8.21588 8.62691 11.4552 8.51564 15ZM11.0383 17.5C11.1951 20.5456 11.8216 23.2322 12.7185 25.2055C13.8114 27.6098 15.0657 28.5 16 28.5C16.9343 28.5 18.1886 27.6098 19.2815 25.2055C20.1784 23.2322 20.8049 20.5456 20.9617 17.5H11.0383ZM20.983 15H11.017C11.1277 11.7487 11.7728 8.87511 12.7185 6.79454C13.8114 4.39021 15.0657 3.5 16 3.5C16.9343 3.5 18.1886 4.39021 19.2815 6.79454C20.2272 8.87511 20.8723 11.7487 20.983 15ZM23.4647 17.5C23.3061 20.8489 22.621 23.9001 21.5574 26.24C21.3601 26.674 21.1428 27.0971 20.9059 27.5006C24.9318 25.781 27.8717 22.0093 28.4109 17.5H23.4647ZM28.4606 15H23.4844C23.3731 11.4552 22.6737 8.21588 21.5574 5.76003C21.3601 5.326 21.1428 4.90291 20.9059 4.49944C25.0797 6.28219 28.0862 10.2707 28.4606 15Z"
fill="currentColor"
/>
</svg>
);
}
29 changes: 29 additions & 0 deletions special-pages/pages/new-tab/app/components/CompanyIcon.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.icon {
display: block;
width: 1rem;
height: 1rem;
border-radius: 50%;
flex-shrink: 0;

img, svg {
display: block;
font-size: 0;
width: 1rem;
height: 1rem;
}

&:has([data-errored=true]) {
outline: 1px solid var(--ntp-surface-border-color);
[data-theme=dark] & {
outline-color: var(--color-white-at-9);
}
}
}

.companyImgIcon {
opacity: 0;
}

.companyImgIcon[data-loaded=true] {
opacity: 1;
}
139 changes: 139 additions & 0 deletions special-pages/pages/new-tab/app/components/ImageWithState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { DDG_DEFAULT_ICON_SIZE, DDG_FALLBACK_ICON, DDG_FALLBACK_ICON_DARK } from '../favorites/constants.js';
import styles from '../favorites/components/Tile.module.css';
import { urlToColor } from '../favorites/getColorForString.js';
import cn from 'classnames';

/**
* @typedef {'loading_favicon_src'
* | 'did_load_favicon_src'
* | 'loading_fallback_img'
* | 'did_load_fallback_img'
* | 'using_fallback_text'
* | 'fallback_img_failed'
* } ImgState
*/
const states = /** @type {Record<ImgState, ImgState>} */ ({
loading_favicon_src: 'loading_favicon_src',
did_load_favicon_src: 'did_load_favicon_src',

loading_fallback_img: 'loading_fallback_img',
did_load_fallback_img: 'did_load_fallback_img',
fallback_img_failed: 'fallback_img_failed',

using_fallback_text: 'using_fallback_text',
});

/**
*
* Loads and displays an image for a given webpage.
*
* @param {Object} props - The props for the image loader.
* @param {string|null|undefined} props.faviconSrc - The URL of the favicon image to load.
* @param {number} props.faviconMax - The maximum size this icon be displayed as
* @param {string} props.title - The title associated with the image.
* @param {'light' | 'dark'} props.theme - the currently applied theme
* @param {'favorite-tile' | 'history-favicon'} props.displayKind
* @param {string|null} props.etldPlusOne - The relevant domain section of the url
*/
export function ImageWithState({ faviconSrc, faviconMax, title, etldPlusOne, theme, displayKind }) {
const size = Math.min(faviconMax, DDG_DEFAULT_ICON_SIZE);
const sizeClass = displayKind === 'favorite-tile' ? styles.faviconLarge : styles.faviconSmall;

// try to use the defined image source
// prettier-ignore
const imgsrc = faviconSrc
? faviconSrc + '?preferredSize=' + size
: null;

// prettier-ignore
const initialState = (() => {
/**
* If the favicon has `src`, always prefer it
*/
if (imgsrc) return states.loading_favicon_src;
/**
* Failing that, use fallback text if possible
*/
if (etldPlusOne) return states.using_fallback_text;
/**
* If we get here, we have no favicon src, and no chance of using fallback text
*/
return states.loading_fallback_img;
})();

const [state, setState] = useState(/** @type {ImgState} */ (initialState));

switch (state) {
/**
* These are the happy paths, where we are loading the favicon source and it does not 404
*/
case states.loading_favicon_src:
case states.did_load_favicon_src: {
if (!imgsrc) {
console.warn('unreachable - must have imgsrc here');
return null;
}
return (
<img
src={imgsrc}
class={cn(styles.favicon, sizeClass)}
alt=""
data-state={state}
onLoad={() => setState(states.did_load_favicon_src)}
onError={() => {
if (etldPlusOne) {
setState(states.using_fallback_text);
} else {
setState(states.loading_fallback_img);
}
}}
/>
);
}
/**
* A fallback can be applied when the `etldPlusOne` is there. For example,
* if `etldPlusOne = 'example.com'`, we can display `Ex` and use the domain name
* to select a background color.
*/
case states.using_fallback_text: {
if (!etldPlusOne) {
console.warn('unreachable - must have etld+1 here');
return null;
}
/** @type {Record<string, string>|undefined} */
let style;
const fallbackColor = urlToColor(etldPlusOne);
if (fallbackColor) {
style = { background: fallbackColor };
}
const chars = etldPlusOne.slice(0, 2);
return (
<div class={cn(styles.favicon, sizeClass, styles.faviconText)} style={style} data-state={state}>
<span>{chars[0]}</span>
<span>{chars[1]}</span>
</div>
);
}
/**
* If we get here, we couldn't load the favicon source OR the fallback text
* So, we default to a globe icon
*/
case states.loading_fallback_img:
case states.did_load_fallback_img: {
return (
<img
src={theme === 'light' ? DDG_FALLBACK_ICON : DDG_FALLBACK_ICON_DARK}
class={cn(styles.favicon, sizeClass)}
alt=""
data-state={state}
onLoad={() => setState(states.did_load_fallback_img)}
onError={() => setState(states.fallback_img_failed)}
/>
);
}
default:
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ export const favoritesExamples = {
<FavoritesConsumer />
</MockFavoritesProvider>
<br />
<MockFavoritesProvider data={favorites.two}>
<FavoritesConsumer />
</MockFavoritesProvider>
<br />
<MockFavoritesProvider data={favorites.single}>
<FavoritesConsumer />
</MockFavoritesProvider>
Expand Down
Loading

0 comments on commit 2145e03

Please sign in to comment.