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:
@@ -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;
|
||||
`;
|
||||
|
@@ -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>
|
||||
|
@@ -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 />}
|
||||
/>
|
||||
)}
|
||||
|
@@ -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 />}
|
||||
/>
|
||||
|
@@ -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 />}
|
||||
/>
|
||||
|
@@ -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
36
web/src/store.tsx
Normal 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;
|
@@ -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);
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user