1
0
mirror of https://github.com/uetchy/namae.git synced 2025-03-17 04:30:31 +09:00

feat: refine footer

This commit is contained in:
uetchy 2020-07-30 13:47:28 +09:00
parent 04725d0a65
commit 0d632a8eb2
7 changed files with 387 additions and 244 deletions

View File

@ -1,206 +1,35 @@
import Tooltip from 'rc-tooltip';
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Redirect, Route, Switch } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { IoIosFlash, IoIosRocket } from 'react-icons/io';
import { Redirect, Route, Switch, useParams } from 'react-router-dom';
import styled, { createGlobalStyle } from 'styled-components';
import Cards from './components/cards';
import {
AvailableIcon,
COLORS as ResultColor,
ResultIcon,
ResultItem,
ResultName,
} from './components/cards/core';
import Footer from './components/Footer'; import Footer from './components/Footer';
import Form from './components/Form'; import Home from './pages/Home';
import Welcome from './components/Welcome'; import Search from './pages/Search';
import { useStoreState } from './store'; import { GlobalStyle } from './theme';
import { mobile } from './util/css'; import { useOpenSearch } from './util/hooks';
import { isStandalone } from './util/pwa'; import { isStandalone } from './util/pwa';
import { sanitize } from './util/text';
export default function App() { export default function App() {
const OpenSearch = useOpenSearch('/opensearch.xml');
return ( return (
<> <>
<GlobalStyle /> <GlobalStyle />
<Helmet> <OpenSearch />
<link
rel="search"
type="application/opensearchdescription+xml"
title="namae"
href="/opensearch.xml"
/>
</Helmet>
<Switch> <Switch>
<Route exact path="/"> <Route exact path="/">
<Home /> <Home />
</Route> </Route>
<Route path="/s/:query"> <Route path="/s/:query">
<Search /> <Search />
</Route> </Route>
<Route path="*"> <Route path="*">
<Redirect to="/" /> <Redirect to="/" />
</Route> </Route>
</Switch> </Switch>
{!isStandalone() && <Footer />} {!isStandalone() && <Footer />}
</> </>
); );
} }
function Home() {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>namae {t('title')}</title>
</Helmet>
<Header>
<Form />
</Header>
<Content>
<Welcome />
</Content>
</>
);
}
function Search() {
const { query } = useParams<{ query: string }>();
const currentQuery = sanitize(query);
const { t } = useTranslation();
return (
<>
<Helmet>
<title>Search for &quot;{currentQuery}&quot; namae</title>
</Helmet>
<Header>
<Form initialValue={currentQuery} />
</Header>
<Content>
<Legend>
<Stat />
<ResultItem color={ResultColor.available}>
<ResultIcon>
<IoIosRocket />
</ResultIcon>
<ResultName>{t('available')}</ResultName>
<AvailableIcon>
<IoIosFlash />
</AvailableIcon>
</ResultItem>
<ResultItem color={ResultColor.unavailable}>
<ResultIcon>
<IoIosRocket />
</ResultIcon>
<ResultName>{t('unavailable')}</ResultName>
</ResultItem>
</Legend>
<Cards query={currentQuery} />
</Content>
</>
);
}
function Stat() {
const totalCount = useStoreState((state) => state.stats.totalCount);
const availableCount = useStoreState((state) => state.stats.availableCount);
const { t } = useTranslation();
const uniqueness = availableCount !== 0 ? availableCount / totalCount : 0.0;
const uniquenessText = ((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');
}
})(uniqueness);
return (
<UniquenessIndicator>
<Tooltip
overlay={t('uniqueness.description')}
placement="top"
trigger={['hover']}>
<span>
{uniquenessText} ({(uniqueness * 100).toFixed(1)} UNIQ)
</span>
</Tooltip>
</UniquenessIndicator>
);
}
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
line-height: 1.625em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #ffffff;
${mobile} {
background: #f5f5f5;
}
}
`;
const Content = styled.div`
padding-top: 100px;
${mobile} {
padding-top: 60px;
}
`;
const Header = styled.header`
padding: 0 40px;
background-image: linear-gradient(180deg, #bda2ff 0%, #1b24cc 99%);
${mobile} {
padding: 0 20px;
}
`;
const Legend = styled.div`
margin-top: -100px;
padding: 100px 0 30px;
display: flex;
flex-direction: row;
justify-content: center;
user-select: none;
cursor: default;
background-color: #f6f6fa;
${mobile} {
flex-direction: column;
align-items: center;
margin-top: -80px;
padding: 70px 0 30px;
background-color: none;
}
> * {
margin: 0 10px 0;
}
`;
const UniquenessIndicator = styled.div`
color: #7b7b7b;
`;

View File

@ -2,27 +2,72 @@ import React from 'react';
import { OutboundLink } from 'react-ga'; import { OutboundLink } from 'react-ga';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaGithub, FaProductHunt, FaTwitter } from 'react-icons/fa'; import { FaGithub, FaProductHunt, FaTwitter } from 'react-icons/fa';
import { GoHeart } from 'react-icons/go';
import styled from 'styled-components'; import styled from 'styled-components';
import { Section } from '../theme';
import { mobile } from '../util/css';
const Footer: React.FC = () => { const Footer: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Container> <Container>
<LangBox> <Pane>
<a href="/?lng=en"> <Title>Language</Title>
<span role="img" aria-label="English"> <LangBox>
🇬🇧 <Links>
</span> <a href="/?lng=en">
</a> <span role="img" aria-label="English">
<a href="/?lng=ja"> 🇬🇧
<span role="img" aria-label="Japanese"> </span>
🇯🇵 </a>
</span> <a href="/?lng=ja">
</a> <span role="img" aria-label="Japanese">
</LangBox> 🇯🇵
</span>
</a>
</Links>
</LangBox>
</Pane>
<Box> <Pane>
<Title>Community</Title>
<ul>
<li>
<OutboundLink
to="https://github.com/uetchy/namae"
eventLabel="GitHub Repo"
aria-label="Go to GitHub repository"
target="_blank"
>
GitHub
</OutboundLink>
</li>
<li>
<OutboundLink
to="https://github.com/uetchy/namae/issues"
eventLabel="GitHub Issues"
aria-label="Go to GitHub Issues"
target="_blank"
>
Issues
</OutboundLink>
</li>
<li>
<OutboundLink
to="https://dev.to/uetchy/give-your-app-slick-name-with-namae-dev-5c4h"
eventLabel="Blog article"
aria-label="Go to blog"
target="_blank"
>
Blog Article
</OutboundLink>
</li>
</ul>
</Pane>
<Pane>
<Title>About</Title>
<p> <p>
Made with{' '} Made with{' '}
<span role="img" aria-label="coffee"> <span role="img" aria-label="coffee">
@ -33,74 +78,109 @@ const Footer: React.FC = () => {
to="https://twitter.com/uechz" to="https://twitter.com/uechz"
eventLabel="Author Page" eventLabel="Author Page"
aria-label="Author page" aria-label="Author page"
target="_blank"> target="_blank"
>
<Bold>Yasuaki Uechi</Bold> <Bold>Yasuaki Uechi</Bold>
</OutboundLink> </OutboundLink>
</p> </p>
</Box> <ShareBox>
<Links>
<ShareBox> <OutboundLink
<Links> to={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
<OutboundLink `namae — ${t('title')}`,
to={`https://twitter.com/intent/tweet?text=${encodeURIComponent( )}&url=${encodeURIComponent('https://namae.dev')}`}
`namae — ${t('title')}`, eventLabel="Tweet"
)}&url=${encodeURIComponent('https://namae.dev')}`} aria-label="Tweet this page"
eventLabel="Tweet" target="_blank"
aria-label="Tweet this page" >
target="_blank"> <FaTwitter />
<FaTwitter /> </OutboundLink>
</OutboundLink> <OutboundLink
<OutboundLink to="https://www.producthunt.com/posts/namae"
to="https://www.producthunt.com/posts/namae" eventLabel="ProductHunt"
eventLabel="ProductHunt" aria-label="Go to ProductHunt page"
aria-label="Go to ProductHunt page" target="_blank"
target="_blank"> >
<FaProductHunt /> <FaProductHunt />
</OutboundLink> </OutboundLink>
<OutboundLink <OutboundLink
to="https://github.com/uetchy/namae" to="https://github.com/uetchy/namae"
eventLabel="GitHub Repo" eventLabel="GitHub Repo"
aria-label="Go to GitHub repository" aria-label="Go to GitHub repository"
target="_blank"> target="_blank"
<FaGithub /> >
</OutboundLink> <FaGithub />
</Links> </OutboundLink>
</ShareBox> <OutboundLink
to="https://github.com/sponsors/uetchy"
eventLabel="GitHub Sponsors"
aria-label="Go to GitHub Sponsors"
target="_blank"
>
<SponsorBadge>
<GoHeart size="1.3rem" />
<span>Sponsor</span>
</SponsorBadge>
</OutboundLink>
</Links>
</ShareBox>
</Pane>
</Container> </Container>
); );
}; };
export default Footer; export default Footer;
const Container = styled.div` const Container = styled(Section)`
--text: #bdbdbd;
--background: #404040;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: center; justify-content: space-around;
margin: 40px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background: var(--background);
color: var(--text);
a { a {
color: black; color: var(--text);
text-decoration: none; text-decoration: none;
} }
ul {
li {
list-style-type: none;
}
}
${mobile} {
flex-direction: column;
}
`; `;
const Box = styled.footer` const Pane = styled.div`
margin-bottom: 10px; font-size: 1rem;
display: flex;
flex-direction: row; ${mobile} {
justify-content: center; margin-bottom: 50px;
}
`;
const Title = styled.h3`
margin-bottom: 15px;
`;
const LangBox = styled.div`
line-height: 1em; line-height: 1em;
font-size: 0.8rem;
`;
const LangBox = styled(Box)`
font-size: 2rem; font-size: 2rem;
margin-bottom: 20px;
`; `;
const ShareBox = styled(Box)` const ShareBox = styled.div`
margin-top: 15px;
line-height: 1em;
font-size: 1.5rem; font-size: 1.5rem;
display: flex;
align-items: center;
`; `;
const Links = styled.div` const Links = styled.div`
@ -108,10 +188,35 @@ const Links = styled.div`
align-items: center; align-items: center;
a { a {
margin: 0 3px; margin-right: 10px;
} }
`; `;
const Bold = styled.span` const Bold = styled.span`
font-weight: bold; font-weight: bold;
`; `;
const SponsorBadge = styled.div`
padding: 5px 13px 5px 10px;
display: flex;
flex-direction: row;
align-items: center;
line-height: 1rem;
font-size: 1rem;
border-radius: 5px;
background-color: #f8f9fa;
color: black;
cursor: pointer;
transition: opacity 200ms ease-out;
:hover {
opacity: 0.8;
}
svg {
color: rgb(236, 69, 171);
margin-right: 5px;
}
`;

26
src/pages/Home.tsx Normal file
View File

@ -0,0 +1,26 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { Content, Header } from '../theme';
import Form from '../components/Form';
import Welcome from '../components/Welcome';
export default function Home() {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>namae {t('title')}</title>
</Helmet>
<Header>
<Form />
</Header>
<Content>
<Welcome />
</Content>
</>
);
}

116
src/pages/Search.tsx Normal file
View File

@ -0,0 +1,116 @@
import Tooltip from 'rc-tooltip';
import React from 'react';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { IoIosFlash, IoIosRocket } from 'react-icons/io';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import Cards from '../components/cards';
import {
AvailableIcon,
COLORS as ResultColor,
ResultIcon,
ResultItem,
ResultName,
} from '../components/cards/core';
import Form from '../components/Form';
import { useStoreState } from '../store';
import { Content, Header } from '../theme';
import { mobile } from '../util/css';
import { sanitize } from '../util/text';
export default function Search() {
const { query } = useParams<{ query: string }>();
const currentQuery = sanitize(query);
const { t } = useTranslation();
return (
<>
<Helmet>
<title>Search for &quot;{currentQuery}&quot; namae</title>
</Helmet>
<Header>
<Form initialValue={currentQuery} />
</Header>
<Content>
<Legend>
<Stat />
<ResultItem color={ResultColor.available}>
<ResultIcon>
<IoIosRocket />
</ResultIcon>
<ResultName>{t('available')}</ResultName>
<AvailableIcon>
<IoIosFlash />
</AvailableIcon>
</ResultItem>
<ResultItem color={ResultColor.unavailable}>
<ResultIcon>
<IoIosRocket />
</ResultIcon>
<ResultName>{t('unavailable')}</ResultName>
</ResultItem>
</Legend>
<Cards query={currentQuery} />
</Content>
</>
);
}
function Stat() {
const totalCount = useStoreState((state) => state.stats.totalCount);
const availableCount = useStoreState((state) => state.stats.availableCount);
const { t } = useTranslation();
const uniqueness = availableCount !== 0 ? availableCount / totalCount : 0.0;
const uniquenessText = ((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');
}
})(uniqueness);
return (
<UniquenessIndicator>
<Tooltip
overlay={t('uniqueness.description')}
placement="top"
trigger={['hover']}
>
<span>
{uniquenessText} ({(uniqueness * 100).toFixed(1)} UNIQ)
</span>
</Tooltip>
</UniquenessIndicator>
);
}
export const Legend = styled.div`
margin-top: -100px;
padding: 100px 0 30px;
display: flex;
flex-direction: row;
justify-content: center;
user-select: none;
cursor: default;
background-color: #f6f6fa;
${mobile} {
flex-direction: column;
align-items: center;
margin-top: -80px;
padding: 70px 0 30px;
background-color: none;
}
> * {
margin: 0 10px 0;
}
`;
export const UniquenessIndicator = styled.div`
color: #7b7b7b;
`;

53
src/theme/index.tsx Normal file
View File

@ -0,0 +1,53 @@
import styled, { createGlobalStyle } from 'styled-components';
import { mobile } from '../util/css';
export const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
line-height: 1.625em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #ffffff;
${mobile} {
background: #f5f5f5;
}
}
`;
export const Content = styled.div`
padding-top: 100px;
${mobile} {
padding-top: 60px;
}
`;
export const Header = styled.header`
padding: 0 40px;
background-image: linear-gradient(180deg, #bda2ff 0%, #1b24cc 99%);
${mobile} {
padding: 0 20px;
}
`;
export const Section = styled.div`
padding: 100px 20vw;
${mobile} {
padding: 60px 40px;
}
`;

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
export function useDeferredState<T>( export function useDeferredState<T>(
duration = 1000, duration = 1000,
@ -19,3 +20,16 @@ export function useDeferredState<T>(
return [response, setInnerValue]; return [response, setInnerValue];
} }
export function useOpenSearch(xmlPath: string) {
return () => (
<Helmet>
<link
rel="search"
type="application/opensearchdescription+xml"
title="namae"
href={xmlPath}
/>
</Helmet>
);
}