1
0
mirror of https://github.com/uetchy/namae.git synced 2025-08-20 09:58:13 +09:00

feat: add uniqueness indicator

This commit is contained in:
2020-03-26 20:22:06 +09:00
parent 2f85792fce
commit ef24e724d8
13 changed files with 297 additions and 48 deletions

View File

@@ -8,18 +8,18 @@ import {IoIosRocket, IoIosFlash} from 'react-icons/io';
import Welcome from './components/Welcome';
import Form from './components/Form';
import Cards from './components/cards';
import Footer from './components/Footer';
import {
ResultItem,
ResultIcon,
ResultName,
COLORS,
COLORS as ResultColor,
AvailableIcon,
} from './components/cards/core';
import Footer from './components/Footer';
import {mobile} from './util/css';
import {isStandalone} from './util/pwa';
import {sanitize} from './util/text';
import {useStoreState} from './store';
export default function App() {
return (
@@ -74,7 +74,8 @@ function Search() {
</Header>
<Content>
<Legend>
<ResultItem color={COLORS.available}>
<Stat />
<ResultItem color={ResultColor.available}>
<ResultIcon>
<IoIosRocket />
</ResultIcon>
@@ -83,7 +84,7 @@ function Search() {
<IoIosFlash />
</AvailableIcon>
</ResultItem>
<ResultItem color={COLORS.unavailable}>
<ResultItem color={ResultColor.unavailable}>
<ResultIcon>
<IoIosRocket />
</ResultIcon>
@@ -96,6 +97,24 @@ function Search() {
);
}
function Stat() {
const totalCount = useStoreState((state) => state.stats.totalCount);
const availableCount = useStoreState((state) => state.stats.availableCount);
const {t} = useTranslation();
const uniqueness = ((n) => {
if (n > 0.7 && n <= 1.0) {
return t('uniqueness.high');
} else if (n > 0.4 && n <= 0.7) {
return t('uniqueness.moderate');
} else {
return t('uniqueness.low');
}
})(availableCount / totalCount);
return <UniquenessIndicator>{uniqueness}</UniquenessIndicator>;
}
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
@@ -150,12 +169,18 @@ const Legend = styled.div`
background-color: #f6f6fa;
${mobile} {
flex-direction: column;
align-items: center;
margin-top: -80px;
padding: 70px 0 30px;
background-color: none;
}
${ResultItem} {
> * {
margin: 0 10px 0;
}
`;
const UniquenessIndicator = styled.div`
color: #7b7b7b;
`;

View File

@@ -1,8 +1,8 @@
import React, {useState, useEffect, Suspense} from 'react';
import styled from 'styled-components';
import useFetch from 'fetch-suspense';
import {Tooltip} from 'react-tippy';
import 'react-tippy/dist/tippy.css';
import Tooltip from 'rc-tooltip';
import 'rc-tooltip/assets/bootstrap.css';
import BarLoader from 'react-spinners/BarLoader';
import {GoInfo} from 'react-icons/go';
import {IoIosFlash} from 'react-icons/io';
@@ -11,6 +11,7 @@ import {OutboundLink} from 'react-ga';
import {sendError, sendExpandEvent} from '../../util/analytics';
import {mobile} from '../../util/css';
import {useStoreActions} from '../../store';
export const COLORS = {
available: '#6e00ff',
@@ -105,6 +106,7 @@ export const DedicatedAvailability: React.FC<{
suffix = '',
icon,
}) => {
const increaseCounter = useStoreActions((actions) => actions.stats.add);
const response = useFetch(
`/availability/${service}/${encodeURIComponent(query || name)}`,
) as Response;
@@ -113,6 +115,10 @@ export const DedicatedAvailability: React.FC<{
throw new APIError(`${service}: ${response.error}`);
}
useEffect(() => {
increaseCounter(response.availability);
}, []);
return (
<Result
title={name}
@@ -147,6 +153,7 @@ export const ExistentialAvailability: React.FC<{
suffix = '',
icon,
}) => {
const increaseCounter = useStoreActions((actions) => actions.stats.add);
const response = useFetch(target, undefined, {metadata: true});
if (response.status !== 404 && response.status !== 200) {
@@ -155,6 +162,10 @@ export const ExistentialAvailability: React.FC<{
const availability = response.status === 404;
useEffect(() => {
increaseCounter(availability);
}, []);
return (
<Result
title={name}
@@ -200,13 +211,7 @@ export const Result: React.FC<{
: COLORS.unavailable;
return (
<ResultContainer>
<Tooltip
title={message}
position="bottom"
arrow={true}
animation="shift"
duration="200"
>
<Tooltip overlay={message} placement="top" trigger={['hover']}>
<ResultItem color={itemColor}>
<ResultIcon>{icon}</ResultIcon>
<ResultName>
@@ -266,13 +271,11 @@ class ErrorBoundary extends React.Component<
return (
<ResultContainer>
<Tooltip
title={`${this.state.message}${
overlay={`${this.state.message}${
this.state.eventId ? ` (${this.state.eventId})` : ''
}`}
position="bottom"
arrow={true}
animation="shift"
duration="200"
placement="top"
trigger={['hover']}
>
<ResultItem color={COLORS.error}>
<ResultIcon>

View File

@@ -19,6 +19,7 @@ const CratesioCard: React.FC<{query: string}> = ({query}) => {
query={`crates.io/api/v1/crates/${name}`}
service="existence"
link={`https://crates.io/crates/${name}`}
message="Go to crates.io"
icon={<DiRust />}
/>
)}

View File

@@ -19,12 +19,14 @@ const LinuxCard: React.FC<{query: string}> = ({query}) => {
<DedicatedAvailability
name={name}
service="launchpad"
message="Go to Launchpad"
link={`https://launchpad.net/ubuntu/+source/${name}`}
icon={<DiUbuntu />}
/>
<DedicatedAvailability
name={name}
service="debian"
message="Go to debian.org"
link={`https://packages.debian.org/buster/${name}`}
icon={<DiDebian />}
/>

View File

@@ -18,6 +18,7 @@ const OcamlCard: React.FC<{query: string}> = ({query}) => {
name={name}
query={`opam.ocaml.org/packages/${name}/`}
service="existence"
message="Go to opam"
link={`https://opam.ocaml.org/packages/${name}/`}
icon={<OcamlIcon />}
/>

View File

@@ -1,25 +1,35 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Router} from 'react-router-dom';
import {StoreProvider, createStore} from 'easy-peasy';
import {createBrowserHistory} from 'history';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {FullScreenSuspense} from './util/suspense';
import {initHistoryWithGA, initSentry} from './util/analytics';
import {wrapHistoryWithGA, initSentry} from './util/analytics';
import {initCrisp} from './util/crip';
import {storeModel} from './store';
import './util/i18n';
initSentry();
initCrisp();
const history = initHistoryWithGA();
const store = createStore(storeModel);
const history = wrapHistoryWithGA(createBrowserHistory());
history.listen(() => {
// reset stats counter
store.getActions().stats.reset();
});
ReactDOM.render(
<FullScreenSuspense>
<Router history={history}>
<App />
</Router>
</FullScreenSuspense>,
<StoreProvider store={store}>
<FullScreenSuspense>
<Router history={history}>
<App />
</Router>
</FullScreenSuspense>
</StoreProvider>,
document.getElementById('root'),
);

36
web/src/store.tsx Normal file
View File

@@ -0,0 +1,36 @@
import {action, createTypedHooks, Action} from 'easy-peasy';
interface StatsModel {
availableCount: number;
totalCount: number;
add: Action<StatsModel, boolean>;
reset: Action<StatsModel, void>;
}
interface StoreModel {
stats: StatsModel;
}
const statsModel: StatsModel = {
availableCount: 0,
totalCount: 0,
add: action((state, isAvailable) => {
state.totalCount += 1;
if (isAvailable) {
state.availableCount += 1;
}
}),
reset: action((state) => {
state.totalCount = 0;
state.availableCount = 0;
}),
};
export const storeModel: StoreModel = {
stats: statsModel,
};
const typedHooks = createTypedHooks<StoreModel>();
export const useStoreActions = typedHooks.useStoreActions;
export const useStoreDispatch = typedHooks.useStoreDispatch;
export const useStoreState = typedHooks.useStoreState;

View File

@@ -1,11 +1,10 @@
import ReactGA from 'react-ga';
import * as Sentry from '@sentry/browser';
import {createBrowserHistory} from 'history';
import {History} from 'history';
const isProduction = process.env.NODE_ENV !== 'development';
export function initHistoryWithGA() {
const history = createBrowserHistory();
export function wrapHistoryWithGA(history: History) {
if (isProduction) {
ReactGA.initialize('UA-28919359-15');
ReactGA.pageview(window.location.pathname + window.location.search);

View File

@@ -5,7 +5,7 @@ import XHR from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import {initReactI18next} from 'react-i18next';
const TRANSLATION_VERSION = '1.13';
const TRANSLATION_VERSION = '1.14';
i18n
.use(Backend)