mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:51:43 +00:00
New welcome page (homepage) for Sourcegraph.com (#1975)
* remove scrolling effects * remove forced dark theme and custom colors * move MainPage and related to enterprise/dotcom/welcome * rename /start to /welcome The name /welcome better conveys that it is for first-time users, not the page where you start each session. * create WelcomeArea with common header * extract company logos * factor out welcome footer * clean up HTML for welcome main page * redirect self-hosted instance /welcome to sourcegraph.com/welcome * update copy on main page
This commit is contained in:
parent
3a8c80f6eb
commit
1cc218f8de
@ -215,9 +215,9 @@ func serveHome(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
|
||||
if envvar.SourcegraphDotComMode() && !actor.FromContext(r.Context()).IsAuthenticated() {
|
||||
// The user is not signed in and tried to access our main site at sourcegraph.com.
|
||||
// Redirect to sourcegraph.com/start so they see general info.
|
||||
http.Redirect(w, r, "/start", http.StatusTemporaryRedirect)
|
||||
// The user is not signed in and tried to access Sourcegraph.com. Redirect to /welcome so
|
||||
// they see the welcome page.
|
||||
http.Redirect(w, r, "/welcome", http.StatusTemporaryRedirect)
|
||||
return nil
|
||||
}
|
||||
// sourcegraph.com (not about) homepage. There is none, redirect them to /search.
|
||||
@ -248,7 +248,7 @@ func serveSignIn(w http.ResponseWriter, r *http.Request) error {
|
||||
return renderTemplate(w, "app.html", common)
|
||||
}
|
||||
|
||||
func serveStart(w http.ResponseWriter, r *http.Request) error {
|
||||
func serveWelcome(w http.ResponseWriter, r *http.Request) error {
|
||||
common, err := newCommon(w, r, "Sourcegraph", serveError)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -258,8 +258,7 @@ func serveStart(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
|
||||
if !envvar.SourcegraphDotComMode() {
|
||||
// The user is signed in and tried to access sourcegraph.com/start,
|
||||
// this page should be a 404 under that situation.
|
||||
// The welcome page only exists on Sourcegraph.com.
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
return renderTemplate(w, "app.html", common)
|
||||
|
||||
@ -64,6 +64,7 @@ const (
|
||||
routeExtensions = "extensions"
|
||||
routeHelp = "help"
|
||||
routeExplore = "explore"
|
||||
routeWelcome = "welcome"
|
||||
|
||||
// Legacy redirects
|
||||
routeLegacyLogin = "login"
|
||||
@ -113,6 +114,7 @@ func newRouter() *mux.Router {
|
||||
r.Path("/").Methods("GET").Name(routeHome)
|
||||
r.PathPrefix("/threads").Methods("GET").Name(routeThreads)
|
||||
r.Path("/start").Methods("GET").Name(routeStart)
|
||||
r.PathPrefix("/welcome").Methods("GET").Name(routeWelcome)
|
||||
r.Path("/search").Methods("GET").Name(routeSearch)
|
||||
r.Path("/search/badge").Methods("GET").Name(routeSearchBadge)
|
||||
r.Path("/search/searches").Methods("GET").Name(routeSearchSearches)
|
||||
@ -185,7 +187,8 @@ func initRouter() {
|
||||
router := newRouter()
|
||||
uirouter.Router = router // make accessible to other packages
|
||||
router.Get(routeHome).Handler(handler(serveHome))
|
||||
router.Get(routeStart).Handler(handler(serveStart))
|
||||
router.Get(routeStart).Handler(staticRedirectHandler("/welcome", http.StatusMovedPermanently))
|
||||
router.Get(routeWelcome).Handler(handler(serveWelcome))
|
||||
router.Get(routeThreads).Handler(handler(serveBasicPageString("Threads - Sourcegraph")))
|
||||
router.Get(uirouter.RouteSignIn).Handler(handler(serveSignIn))
|
||||
router.Get(uirouter.RouteSignUp).Handler(handler(serveBasicPageString("Sign up - Sourcegraph")))
|
||||
|
||||
4
go.mod
4
go.mod
@ -10,11 +10,9 @@ require (
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||
github.com/coreos/go-semver v0.2.0
|
||||
github.com/cosiner/argv v0.0.1 // indirect
|
||||
github.com/crewjam/saml v0.0.0-20180831135026-ebc5f787b786
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/daviddengcn/go-colortext v0.0.0-20171126034257-17e75f6184bc
|
||||
github.com/derekparker/delve v1.1.0 // indirect
|
||||
github.com/dghubble/gologin v1.0.2-0.20181013174641-0e442dd5bb73
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/die-net/lrucache v0.0.0-20190123005519-19a39ef22a11
|
||||
@ -82,7 +80,6 @@ require (
|
||||
github.com/neelance/parallel v0.0.0-20160708114440-4de9ce63d14c
|
||||
github.com/opentracing-contrib/go-stdlib v0.0.0-20190104202730-77df8e8e70b4
|
||||
github.com/opentracing/opentracing-go v1.0.2
|
||||
github.com/peterh/liner v1.1.0 // indirect
|
||||
github.com/peterhellberg/link v1.0.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
@ -118,7 +115,6 @@ require (
|
||||
github.com/xeonx/timeago v1.0.0-rc3
|
||||
github.com/zenazn/goji v0.9.0 // indirect
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190104202753-ff983b9c42bc
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e
|
||||
golang.org/x/oauth2 v0.0.0-20190201180606-99b60b757ec1
|
||||
|
||||
10
go.sum
10
go.sum
@ -43,8 +43,6 @@ github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmq
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cosiner/argv v0.0.1 h1:2iAFN+sWPktbZ4tvxm33Ei8VY66FPCxdOxpncUGpAXE=
|
||||
github.com/cosiner/argv v0.0.1/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
|
||||
github.com/crewjam/saml v0.0.0-20180831135026-ebc5f787b786 h1:8OVABJfT9iJh/uHeYlk1HWugxt7j50JPwW2uLOV9Yqs=
|
||||
github.com/crewjam/saml v0.0.0-20180831135026-ebc5f787b786/go.mod h1:w5eu+HNtubx+kRpQL6QFT2F3yIFfYVe6+EzOFVU7Hko=
|
||||
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
||||
@ -62,8 +60,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/daviddengcn/go-colortext v0.0.0-20171126034257-17e75f6184bc h1:nqMZEdowWmtK9ysqvFibHJ56mTprkyE5c/6q8ZHwLc0=
|
||||
github.com/daviddengcn/go-colortext v0.0.0-20171126034257-17e75f6184bc/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
|
||||
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
|
||||
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I=
|
||||
@ -328,8 +324,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
@ -367,8 +361,6 @@ github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM=
|
||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterh/liner v1.1.0 h1:f+aAedNJA6uk7+6rXsYBnhdo4Xux7ESLe+kcuVUF5os=
|
||||
github.com/peterh/liner v1.1.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/peterhellberg/link v1.0.0 h1:mUWkiegowUXEcmlb+ybF75Q/8D2Y0BjZtR8cxoKhaQo=
|
||||
github.com/peterhellberg/link v1.0.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -504,8 +496,6 @@ go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 h1:Pn8fQdvx+z1avAi7fdM2kRYWQNxGlavNDSyzrQg2SsU=
|
||||
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
|
||||
golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
"@types/babel__core": "7.0.4",
|
||||
"@types/chai": "4.1.7",
|
||||
"@types/chai-as-promised": "7.1.0",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/cpy": "5.1.0",
|
||||
"@types/d3-axis": "1.0.11",
|
||||
"@types/d3-scale": "2.1.0",
|
||||
@ -174,6 +175,7 @@
|
||||
"@sqs/jsonc-parser": "^1.0.3",
|
||||
"abortable-rx": "^1.0.9",
|
||||
"bootstrap": "^4.1.3",
|
||||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.0.7",
|
||||
"d3-axis": "^1.0.7",
|
||||
"d3-scale": "^2.0.0",
|
||||
|
||||
@ -10,7 +10,9 @@ import { PlatformContextProps } from '../platform/context'
|
||||
import { ActionItem, ActionItemProps } from './ActionItem'
|
||||
import { ActionsState } from './actions'
|
||||
|
||||
export interface ActionsProps extends ExtensionsControllerProps, PlatformContextProps {
|
||||
export interface ActionsProps
|
||||
extends ExtensionsControllerProps<'executeCommand' | 'services'>,
|
||||
PlatformContextProps<'forceUpdateTooltip'> {
|
||||
menu: ContributableMenu
|
||||
scope?: ContributionScope
|
||||
actionItemClass?: string
|
||||
|
||||
@ -14,7 +14,9 @@ import { getContributedActionItems } from '../contributions/contributions'
|
||||
import { ExtensionsControllerProps } from '../extensions/controller'
|
||||
import { PlatformContextProps } from '../platform/context'
|
||||
|
||||
interface Props extends ExtensionsControllerProps, PlatformContextProps {
|
||||
interface Props
|
||||
extends ExtensionsControllerProps<'services' | 'executeCommand'>,
|
||||
PlatformContextProps<'forceUpdateTooltip'> {
|
||||
/** The menu whose commands to display. */
|
||||
menu: ContributableMenu
|
||||
|
||||
|
||||
@ -64,8 +64,6 @@ export interface LayoutProps
|
||||
|
||||
isLightTheme: boolean
|
||||
onThemeChange: () => void
|
||||
onMainPage: (mainPage: boolean) => void
|
||||
isMainPage: boolean
|
||||
navbarSearchQuery: string
|
||||
onNavbarQueryChange: (query: string) => void
|
||||
|
||||
|
||||
@ -240,7 +240,6 @@ hr {
|
||||
@import './global/GlobalAlerts';
|
||||
@import './global/GlobalDebug';
|
||||
@import './docSite/DocSitePage';
|
||||
@import './search/input/MainPage';
|
||||
@import './search/input/ScopePage';
|
||||
@import './search/input/SearchPage';
|
||||
@import './search/results/SearchResults';
|
||||
|
||||
@ -73,11 +73,6 @@ interface SourcegraphWebAppState extends PlatformContextProps, SettingsCascadePr
|
||||
*/
|
||||
isLightTheme: boolean
|
||||
|
||||
/**
|
||||
* Whether the user is on MainPage and therefore not logged in
|
||||
*/
|
||||
isMainPage: boolean
|
||||
|
||||
/**
|
||||
* The current search query in the navbar.
|
||||
*/
|
||||
@ -108,7 +103,6 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
|
||||
extensionsController: createExtensionsController(platformContext),
|
||||
settingsCascade: EMPTY_SETTINGS_CASCADE,
|
||||
viewerSubject: SITE_SUBJECT_NO_ADMIN,
|
||||
isMainPage: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,15 +155,9 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
// Always show MainPage in dark theme look
|
||||
if (this.state.isMainPage && this.state.isLightTheme) {
|
||||
document.body.classList.remove('theme-light')
|
||||
document.body.classList.add('theme-dark')
|
||||
} else {
|
||||
localStorage.setItem(LIGHT_THEME_LOCAL_STORAGE_KEY, this.state.isLightTheme + '')
|
||||
document.body.classList.toggle('theme-light', this.state.isLightTheme)
|
||||
document.body.classList.toggle('theme-dark', !this.state.isLightTheme)
|
||||
}
|
||||
localStorage.setItem(LIGHT_THEME_LOCAL_STORAGE_KEY, this.state.isLightTheme + '')
|
||||
document.body.classList.toggle('theme-light', this.state.isLightTheme)
|
||||
document.body.classList.toggle('theme-dark', !this.state.isLightTheme)
|
||||
}
|
||||
|
||||
public render(): React.ReactFragment | null {
|
||||
@ -222,8 +210,6 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
|
||||
// Theme
|
||||
isLightTheme={this.state.isLightTheme}
|
||||
onThemeChange={this.onThemeChange}
|
||||
isMainPage={this.state.isMainPage}
|
||||
onMainPage={this.onMainPage}
|
||||
// Search query
|
||||
navbarSearchQuery={this.state.navbarSearchQuery}
|
||||
onNavbarQueryChange={this.onNavbarQueryChange}
|
||||
@ -246,10 +232,6 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
|
||||
this.setState(state => ({ isLightTheme: !state.isLightTheme }))
|
||||
}
|
||||
|
||||
private onMainPage = (mainPage: boolean) => {
|
||||
this.setState(() => ({ isMainPage: mainPage }))
|
||||
}
|
||||
|
||||
private onNavbarQueryChange = (navbarSearchQuery: string) => {
|
||||
this.setState({ navbarSearchQuery })
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ export function refreshAuthenticatedUser(): Observable<never> {
|
||||
)
|
||||
}
|
||||
|
||||
const initialSiteConfigAuthPublic = window.context.critical['auth.public']
|
||||
const initialSiteConfigAuthPublic = window.context ? window.context.critical['auth.public'] : false // default to false in tests
|
||||
|
||||
/**
|
||||
* Whether auth is required to perform any action.
|
||||
@ -67,7 +67,7 @@ const initialSiteConfigAuthPublic = window.context.critical['auth.public']
|
||||
export const authRequired = authenticatedUser.pipe(map(user => user === null && !initialSiteConfigAuthPublic))
|
||||
|
||||
// Populate authenticatedUser.
|
||||
if (window.context.isAuthenticatedUser) {
|
||||
if (window.context && window.context.isAuthenticatedUser) {
|
||||
refreshAuthenticatedUser()
|
||||
.toPromise()
|
||||
.then(() => void 0, err => console.error(err))
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
@import './SourcegraphWebApp.scss'; // Need .scss extension because of https://github.com/webpack-contrib/sass-loader/issues/556
|
||||
|
||||
// Enterprise styles
|
||||
@import './enterprise/dotcom/welcome/WelcomeArea';
|
||||
@import './enterprise/extensions/registry/RegistryArea';
|
||||
@import './enterprise/extensions/registry/RegistryNewExtensionPage';
|
||||
@import './enterprise/extensions/extension/RegistryExtensionManagePage';
|
||||
|
||||
1
web/src/enterprise/dotcom/welcome/WelcomeArea.scss
Normal file
1
web/src/enterprise/dotcom/welcome/WelcomeArea.scss
Normal file
@ -0,0 +1 @@
|
||||
@import './WelcomeMainPage';
|
||||
91
web/src/enterprise/dotcom/welcome/WelcomeArea.tsx
Normal file
91
web/src/enterprise/dotcom/welcome/WelcomeArea.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
|
||||
import H from 'history'
|
||||
import ChevronLeftIcon from 'mdi-react/ChevronLeftIcon'
|
||||
import MapSearchIcon from 'mdi-react/MapSearchIcon'
|
||||
import React from 'react'
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ExtensionsControllerProps } from '../../../../../shared/src/extensions/controller'
|
||||
import * as GQL from '../../../../../shared/src/graphql/schema'
|
||||
import { PlatformContextProps } from '../../../../../shared/src/platform/context'
|
||||
import { ErrorBoundary } from '../../../components/ErrorBoundary'
|
||||
import { HeroPage } from '../../../components/HeroPage'
|
||||
import { RouteDescriptor } from '../../../util/contributions'
|
||||
import { WelcomeAreaFooter } from './WelcomeAreaFooter'
|
||||
|
||||
const NotFoundPage = () => <HeroPage icon={MapSearchIcon} title="404: Not Found" />
|
||||
|
||||
export interface WelcomeAreaRoute extends RouteDescriptor<WelcomeAreaRouteContext> {}
|
||||
|
||||
export interface WelcomeAreaProps extends ExtensionsControllerProps, PlatformContextProps, RouteComponentProps<{}> {
|
||||
authenticatedUser: GQL.IUser | null
|
||||
isLightTheme: boolean
|
||||
routes: ReadonlyArray<WelcomeAreaRoute>
|
||||
location: H.Location
|
||||
history: H.History
|
||||
}
|
||||
|
||||
export interface WelcomeAreaRouteContext extends WelcomeAreaProps {}
|
||||
|
||||
/**
|
||||
* The welcome area, which contains general product information.
|
||||
*/
|
||||
export class WelcomeArea extends React.PureComponent<WelcomeAreaProps> {
|
||||
public render(): JSX.Element | null {
|
||||
if (!window.context.sourcegraphDotComMode) {
|
||||
return (
|
||||
<HeroPage
|
||||
icon={MapSearchIcon}
|
||||
title="No welcome page"
|
||||
detail={
|
||||
<p>
|
||||
Visit{' '}
|
||||
<a href="https://sourcegraph.com/welcome" target="_blank">
|
||||
sourcegraph.com/welcome
|
||||
</a>{' '}
|
||||
instead.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const { children, ...context } = this.props
|
||||
return (
|
||||
<div className="welcome-area container">
|
||||
{this.props.location.pathname !== '/welcome' && (
|
||||
<nav className="d-block my-2">
|
||||
<Link to="/welcome" className="py-2 pr-2">
|
||||
<ChevronLeftIcon className="icon-inline" />
|
||||
Welcome
|
||||
</Link>
|
||||
</nav>
|
||||
)}
|
||||
<ErrorBoundary location={this.props.location}>
|
||||
<React.Suspense fallback={<LoadingSpinner className="icon-inline my-2 d-block mx-auto" />}>
|
||||
<Switch>
|
||||
{this.props.routes.map(
|
||||
({ path, exact, render, condition = () => true }) =>
|
||||
condition(context) && (
|
||||
<Route
|
||||
path={this.props.match.url + path}
|
||||
key="hardcoded-key" // see https://github.com/ReactTraining/react-router/issues/4578#issuecomment-334489490
|
||||
exact={exact}
|
||||
// tslint:disable-next-line:jsx-no-lambda
|
||||
render={routeComponentProps => (
|
||||
<>
|
||||
{render({ ...context, ...routeComponentProps })}
|
||||
<WelcomeAreaFooter isLightTheme={this.props.isLightTheme} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Route component={NotFoundPage} key="hardcoded-key" />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
135
web/src/enterprise/dotcom/welcome/WelcomeAreaFooter.tsx
Normal file
135
web/src/enterprise/dotcom/welcome/WelcomeAreaFooter.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export const WelcomeAreaFooter: React.FunctionComponent<{ isLightTheme: boolean }> = ({ isLightTheme }) => (
|
||||
<>
|
||||
<div className="row mt-5 pt-4 border-top">
|
||||
<div className="col-sm-4 col-m-4 col-lg-4">
|
||||
<img
|
||||
className="mb-2"
|
||||
src={
|
||||
isLightTheme
|
||||
? 'https://about.sourcegraph.com/sourcegraph/logo.svg'
|
||||
: 'https://about.sourcegraph.com/sourcegraph/logo--light.svg'
|
||||
}
|
||||
/>
|
||||
<p>
|
||||
<a href="mailto:hi@sourcegraph.com" target="_blank">
|
||||
hi@sourcegraph.com
|
||||
</a>
|
||||
<br />
|
||||
142 Minna St, 2nd Floor
|
||||
<br />
|
||||
San Francisco, CA 94105 (USA)
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
|
||||
<h3 className="mb-0">Features</h3>
|
||||
<ul className="list-unstyled">
|
||||
<li>
|
||||
<Link to="/welcome/search">Code search</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/welcome/code-intelligence">Code intelligence</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/welcome/integrations">Integrations</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/pricing" target="_blank">
|
||||
Enterprise
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
|
||||
<h3 className="mb-0">Community</h3>
|
||||
<ul className="list-unstyled">
|
||||
<li>
|
||||
<a href="https://github.com/sourcegraph/sourcegraph" target="_blank">
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/blog" target="_blank">
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/srcgraph" target="_blank">
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.linkedin.com/company/4803356/" target="_blank">
|
||||
LinkedIn
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
|
||||
<h3 className="mb-0">Company</h3>
|
||||
<ul className="list-unstyled">
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/plan" target="_blank">
|
||||
Master plan
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/about" target="_blank">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/contact" target="_blank">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/jobs" target="_blank">
|
||||
Careers
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
|
||||
<h3 className="mb-0">Resources</h3>
|
||||
<ul className="list-unstyled">
|
||||
<li>
|
||||
<a href="https://docs.sourcegraph.com" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/CHANGELOG.md"
|
||||
target="_blank"
|
||||
>
|
||||
Changelog
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/pricing" target="_blank">
|
||||
Pricing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://about.sourcegraph.com/security" target="_blank">
|
||||
Security
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted mt-3 pb-2">
|
||||
<a href="https://about.sourcegraph.com/terms" target="_blank">
|
||||
Terms
|
||||
</a>{' '}
|
||||
-{' '}
|
||||
<a href="https://about.sourcegraph.com/privacy" target="_blank">
|
||||
Privacy
|
||||
</a>{' '}
|
||||
- Copyright © 2018 Sourcegraph, Inc.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
23
web/src/enterprise/dotcom/welcome/WelcomeMainPage.scss
Normal file
23
web/src/enterprise/dotcom/welcome/WelcomeMainPage.scss
Normal file
@ -0,0 +1,23 @@
|
||||
@import './WelcomeMainPageDemos';
|
||||
@import './WelcomeMainPageLogos';
|
||||
|
||||
.welcome-main-page {
|
||||
&__logo-mark {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
position: absolute;
|
||||
top: -1.5rem; // make CTAs flush with header text
|
||||
}
|
||||
&__header {
|
||||
font-size: 24px;
|
||||
}
|
||||
&__demo {
|
||||
min-height: 540px; // reduce jitter while page is loading
|
||||
}
|
||||
}
|
||||
|
||||
.theme-dark .welcome-main-page {
|
||||
&__sign-up {
|
||||
color: #ffffff; // otherwise the signup CTA link text is indistinguishable from text-muted
|
||||
}
|
||||
}
|
||||
157
web/src/enterprise/dotcom/welcome/WelcomeMainPage.tsx
Normal file
157
web/src/enterprise/dotcom/welcome/WelcomeMainPage.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import * as H from 'history'
|
||||
import CloudUploadIcon from 'mdi-react/CloudUploadIcon'
|
||||
import * as React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ExtensionsControllerProps } from '../../../../../shared/src/extensions/controller'
|
||||
import * as GQL from '../../../../../shared/src/graphql/schema'
|
||||
import { PlatformContextProps } from '../../../../../shared/src/platform/context'
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { WelcomeMainPageDemos } from './WelcomeMainPageDemos'
|
||||
import { WelcomeMainPageLogos } from './WelcomeMainPageLogos'
|
||||
|
||||
// Lambdas are OK in this component because it is not performance sensitive and using them
|
||||
// simplifies the code.
|
||||
//
|
||||
// tslint:disable:jsx-no-lambda
|
||||
|
||||
interface Props extends ExtensionsControllerProps, PlatformContextProps {
|
||||
authenticatedUser: GQL.IUser | null
|
||||
isLightTheme: boolean
|
||||
location: H.Location
|
||||
history: H.History
|
||||
}
|
||||
|
||||
/**
|
||||
* The welcome main page, which describes Sourcegraph functionality and other general information.
|
||||
*/
|
||||
export class WelcomeMainPage extends React.Component<Props> {
|
||||
public render(): JSX.Element | null {
|
||||
return (
|
||||
<div className="welcome-main-page">
|
||||
<section className="hero-section">
|
||||
<div className="container hero-container mt-5 pt-3">
|
||||
<div className="row justify-content-md-center">
|
||||
<div className="col-md-7 col-lg-6 mr-lg-4 mb-4">
|
||||
<img
|
||||
className="welcome-main-page__logo-mark mb-1"
|
||||
src="/.assets/img/sourcegraph-mark.svg"
|
||||
/>
|
||||
<h2 className="welcome-main-page__header mt-2">
|
||||
<span className="font-weight-normal">
|
||||
Search, navigate, and review code.
|
||||
</span>{' '}
|
||||
Find answers.
|
||||
</h2>
|
||||
<p>Sourcegraph is a web-based code search and navigation tool for dev teams.</p>
|
||||
<ul className="pl-3">
|
||||
<li>
|
||||
<strong>Code search:</strong> fast, cross-repository, on any commit/branch (no
|
||||
indexing delay), with support for regexps, diffs, and{' '}
|
||||
<a href="https://docs.sourcegraph.com/user/search/queries" target="_blank">
|
||||
filters
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Code navigation:</strong> go-to-definition and find-references for{' '}
|
||||
<a
|
||||
href="https://sourcegraph.com/extensions?query=category%3A%22Programming+languages%22"
|
||||
target="_blank"
|
||||
>
|
||||
all major languages
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Deep integrations</strong> with GitHub, GitHub Enterprise, GitLab,
|
||||
Bitbucket Server, Phabricator, etc., plus a{' '}
|
||||
<a href="https://docs.sourcegraph.com/extensions" target="_blank">
|
||||
powerful extension API
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/sourcegraph/sourcegraph" target="_blank">
|
||||
Open-source
|
||||
</a>
|
||||
, self-hosted, and free (
|
||||
<a href="https://about.sourcegraph.com/pricing" target="_blank">
|
||||
Enterprise
|
||||
</a>{' '}
|
||||
upgrade available)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-1">
|
||||
<a href="https://docs.sourcegraph.com/user/tour" target="_blank">
|
||||
See how it's used
|
||||
</a>{' '}
|
||||
to build better software faster at:
|
||||
</p>
|
||||
<div className="welcome-main-page__customer-logos d-flex align-items-center pl-2">
|
||||
<WelcomeMainPageLogos isLightTheme={this.props.isLightTheme} />
|
||||
<span className="small text-muted">
|
||||
…and thousands of other organizations.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-5 col-lg-4 mb-4">
|
||||
<div className="mt-3">
|
||||
<a
|
||||
className="btn btn-primary btn-lg font-weight-bold mb-1 d-inline-flex align-items-center text-nowrap flex-wrap justify-content-center"
|
||||
href="https://docs.sourcegraph.com/#quickstart"
|
||||
onClick={() => eventLogger.log('WelcomeDeploySelfHosted')}
|
||||
>
|
||||
<CloudUploadIcon className="icon-inline mr-2" /> Deploy self-hosted Sourcegraph
|
||||
</a>
|
||||
<small className="text-muted d-block">
|
||||
For use with your organization's private code. Runs securely on your infra (in a
|
||||
single Docker container or on a cluster).
|
||||
</small>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<a
|
||||
className="btn btn-secondary mb-1 d-inline-flex align-items-center"
|
||||
href="https://docs.sourcegraph.com/integration/browser_extension"
|
||||
onClick={() => eventLogger.log('WelcomeInstallBrowserExtension')}
|
||||
>
|
||||
Install browser extension
|
||||
</a>
|
||||
<small className="text-muted d-block">
|
||||
Adds go-to-definition and find-references to GitHub and other code hosts. For
|
||||
private code, connect it to your self-hosted Sourcegraph instance.
|
||||
</small>
|
||||
</div>
|
||||
{!this.props.authenticatedUser && (
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
to="/sign-up"
|
||||
target="_blank"
|
||||
className="welcome-main-page__sign-up"
|
||||
onClick={() => eventLogger.log('WelcomeSignUpForSourcegraphDotCom')}
|
||||
>
|
||||
Sign up on Sourcegraph.com
|
||||
</Link>
|
||||
<small className="text-muted d-block">
|
||||
A public Sourcegraph instance for public code only.
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row justify-content-md-center mt-3 pt-5 border-top">
|
||||
<WelcomeMainPageDemos
|
||||
className="col-md-12 col-lg-10"
|
||||
location={this.props.location}
|
||||
history={this.props.history}
|
||||
/>
|
||||
<iframe
|
||||
className="welcome-main-page__demo col-md-12 col-lg-10 d-none"
|
||||
src="https://www.youtube.com/embed/Pfy2CjeJ2N4"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
13
web/src/enterprise/dotcom/welcome/WelcomeMainPageDemos.scss
Normal file
13
web/src/enterprise/dotcom/welcome/WelcomeMainPageDemos.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.welcome-main-page-demos {
|
||||
&__item {
|
||||
touch-action: manipulation; // remove iOS tap delay
|
||||
}
|
||||
&__video {
|
||||
// To avoid jitter during loading/switching, calculate height of video based on page
|
||||
// dimensions and video aspect ratio.
|
||||
min-height: calc((100vw - (12px * 4)) * (1596 / 2535));
|
||||
@media screen and (min-width: 1000px) {
|
||||
min-height: 550px;
|
||||
}
|
||||
}
|
||||
}
|
||||
83
web/src/enterprise/dotcom/welcome/WelcomeMainPageDemos.tsx
Normal file
83
web/src/enterprise/dotcom/welcome/WelcomeMainPageDemos.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import classnames from 'classnames'
|
||||
import H from 'history'
|
||||
import PlayCircleOutlineIcon from 'mdi-react/PlayCircleOutlineIcon'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
|
||||
interface Props {
|
||||
className: string
|
||||
location: H.Location
|
||||
history: H.History
|
||||
}
|
||||
|
||||
const BASE_VIDEO_URL = 'https://storage.googleapis.com/sourcegraph-assets/video/welcome/video'
|
||||
|
||||
// The mp4 videos are 2535x1596 at 20fps.
|
||||
//
|
||||
// To convert videos from mp4 (from kazam) to m4v (for iPhone):
|
||||
//
|
||||
// ffmpeg -i INPUT_FILE -pix_fmt yuv420p -vf "scale=-2:720:flags=lanczos" -vcodec libx264 -level 3.2 -profile:v main -preset medium -crf 23 -x264-params ref=4 -movflags +faststart OUTPUT_FILE
|
||||
//
|
||||
// To upload files:
|
||||
//
|
||||
// gsutil cp -a public-read -r INPUT_FILES gs://sourcegraph-assets/video/welcome/video/
|
||||
const VIDEOS: { title: string; hash: string; filename: string }[] = [
|
||||
{
|
||||
title: 'Code navigation',
|
||||
hash: 'code-navigation',
|
||||
filename: 'Welcome-CodeNavigation',
|
||||
},
|
||||
{
|
||||
title: 'Code search',
|
||||
hash: 'code-search',
|
||||
filename: 'Welcome-Search',
|
||||
},
|
||||
{
|
||||
title: 'GitHub integration',
|
||||
hash: 'github-integration',
|
||||
filename: 'Welcome-GitHub',
|
||||
},
|
||||
]
|
||||
|
||||
export class WelcomeMainPageDemos extends React.PureComponent<Props> {
|
||||
public render(): JSX.Element | null {
|
||||
const activeTab = this.props.location.hash.replace(/^#/, '') || VIDEOS[0].hash
|
||||
const video = VIDEOS.find(v => v.hash === activeTab) || VIDEOS[0]
|
||||
return (
|
||||
<div className={`welcome-main-page-demos ${this.props.className}`}>
|
||||
<ul className="nav nav-pills text-nowrap justify-content-md-center">
|
||||
<li className="nav-item disabled text-muted d-flex align-items-center mr-2 mb-2">
|
||||
<PlayCircleOutlineIcon className="mr-2" /> Demos:
|
||||
</li>
|
||||
{VIDEOS.map(({ title, hash }, i) => (
|
||||
<li className="nav-item" key={i}>
|
||||
<Link
|
||||
to={{ hash }}
|
||||
className={classnames('welcome-main-page-demos__item nav-link border mx-1 mb-2', {
|
||||
active: activeTab === hash,
|
||||
})}
|
||||
// tslint:disable-next-line:jsx-no-lambda
|
||||
onClick={() => eventLogger.log('WelcomeMainPageDemosVideo', { hash })}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<video
|
||||
key={video.hash}
|
||||
autoPlay={true}
|
||||
muted={true}
|
||||
loop={true}
|
||||
playsInline={true}
|
||||
className="welcome-main-page-demos__video w-100 h-auto"
|
||||
>
|
||||
<source src={`${BASE_VIDEO_URL}/${video.filename}.mp4`} type="video/mp4" />
|
||||
<source src={`${BASE_VIDEO_URL}/${video.filename}.m4v`} type="video/x-m4v" />
|
||||
Demo video playback is not supported on your browser.
|
||||
</video>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
18
web/src/enterprise/dotcom/welcome/WelcomeMainPageLogos.scss
Normal file
18
web/src/enterprise/dotcom/welcome/WelcomeMainPageLogos.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.welcome-main-page-logos {
|
||||
&__logo {
|
||||
flex: 0 0 auto;
|
||||
&-1 {
|
||||
height: 1.5rem;
|
||||
}
|
||||
&-2 {
|
||||
height: 2rem;
|
||||
// stylelint-disable-next-line declaration-property-unit-whitelist
|
||||
margin-top: 5px; // equalize perceived text baseline of customer logos
|
||||
}
|
||||
&-3 {
|
||||
height: 3.5rem;
|
||||
// stylelint-disable-next-line declaration-property-unit-whitelist
|
||||
margin-top: 6px; // equalize perceived text baseline of customer logos
|
||||
}
|
||||
}
|
||||
}
|
||||
33
web/src/enterprise/dotcom/welcome/WelcomeMainPageLogos.tsx
Normal file
33
web/src/enterprise/dotcom/welcome/WelcomeMainPageLogos.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { shuffle } from 'lodash'
|
||||
import React from 'react'
|
||||
import { Logo1, Logo2, Logo3 } from './logos'
|
||||
|
||||
// Shuffle logos because we love all of them infinitely. :)
|
||||
const LOGOS: {
|
||||
component: React.ComponentType<{ className: string; isLightTheme: boolean }>
|
||||
className: string
|
||||
}[] = shuffle([
|
||||
{
|
||||
component: Logo1,
|
||||
className: 'welcome-main-page-logos__logo-1 mr-3',
|
||||
},
|
||||
{
|
||||
component: Logo2,
|
||||
className: 'welcome-main-page-logos__logo-2 mr-3',
|
||||
},
|
||||
{
|
||||
component: Logo3,
|
||||
className: 'welcome-main-page-logos__logo-3 mr-3',
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* The logos for the welcome main page.
|
||||
*/
|
||||
export const WelcomeMainPageLogos: React.FunctionComponent<{ isLightTheme: boolean }> = ({ isLightTheme }) => (
|
||||
<>
|
||||
{LOGOS.map(({ component: C, className }, i) => (
|
||||
<C key={i} className={`welcome-main-page-logos__logo ${className}`} isLightTheme={isLightTheme} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
133
web/src/enterprise/dotcom/welcome/logos.tsx
Normal file
133
web/src/enterprise/dotcom/welcome/logos.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
isLightTheme: boolean
|
||||
className: string
|
||||
}
|
||||
|
||||
// These components are intentionally not named after the companies, to avoid people reusing them
|
||||
// and thinking they are the official SVGs for these company logos.
|
||||
|
||||
export const Logo1: React.FunctionComponent<Props> = ({ isLightTheme, className }) => {
|
||||
const fill = isLightTheme ? '#010202' : 'white'
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="68px"
|
||||
height="36px"
|
||||
viewBox="0 0 926.906 321.777"
|
||||
className={className}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M53.328,229.809c3.917,10.395,9.34,19.283,16.27,26.664c6.93,7.382,15.14,13.031,24.63,16.948
|
||||
c9.491,3.917,19.81,5.875,30.958,5.875c10.847,0,21.015-2.034,30.506-6.102s17.776-9.792,24.856-17.173
|
||||
c7.08-7.382,12.579-16.194,16.496-26.438c3.917-10.244,5.875-21.692,5.875-34.347V0h47.453v316.354h-47.001v-29.376
|
||||
c-10.545,11.147-22.974,19.734-37.285,25.761c-14.312,6.025-29.752,9.038-46.323,9.038c-16.873,0-32.615-2.938-47.228-8.813
|
||||
c-14.612-5.875-27.267-14.235-37.962-25.082S15.441,264.006,9.265,248.79C3.088,233.575,0,216.628,0,197.947V0h47.453v195.236
|
||||
C47.453,207.891,49.411,219.414,53.328,229.809z"
|
||||
/>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M332.168,0v115.243c10.545-10.545,22.748-18.905,36.607-25.082s28.924-9.265,45.193-9.265
|
||||
c16.873,0,32.689,3.163,47.453,9.49c14.763,6.327,27.567,14.914,38.414,25.761s19.434,23.651,25.761,38.414
|
||||
c6.327,14.764,9.49,30.431,9.49,47.002c0,16.57-3.163,32.162-9.49,46.774c-6.327,14.613-14.914,27.343-25.761,38.188
|
||||
c-10.847,10.847-23.651,19.434-38.414,25.761c-14.764,6.327-30.581,9.49-47.453,9.49c-16.27,0-31.409-3.088-45.419-9.265
|
||||
c-14.01-6.176-26.288-14.537-36.833-25.082v28.924h-45.193V0H332.168z M337.365,232.746c4.067,9.642,9.717,18.078,16.948,25.309
|
||||
c7.231,7.231,15.667,12.956,25.308,17.174c9.642,4.218,20.036,6.327,31.184,6.327c10.847,0,21.09-2.109,30.731-6.327
|
||||
s18.001-9.942,25.083-17.174c7.08-7.23,12.729-15.667,16.947-25.309c4.218-9.641,6.327-20.035,6.327-31.183
|
||||
c0-11.148-2.109-21.618-6.327-31.41s-9.867-18.303-16.947-25.534c-7.081-7.23-15.441-12.88-25.083-16.947
|
||||
s-19.885-6.102-30.731-6.102c-10.846,0-21.09,2.034-30.731,6.102s-18.077,9.717-25.309,16.947
|
||||
c-7.23,7.231-12.955,15.742-17.173,25.534c-4.218,9.792-6.327,20.262-6.327,31.41C331.264,212.711,333.298,223.105,337.365,232.746
|
||||
z"
|
||||
/>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M560.842,155.014c6.025-14.462,14.312-27.191,24.856-38.188s23.049-19.659,37.511-25.986
|
||||
s30.129-9.49,47.001-9.49c16.571,0,31.937,3.013,46.098,9.038c14.16,6.026,26.362,14.387,36.606,25.083
|
||||
c10.244,10.695,18.229,23.35,23.952,37.962c5.725,14.613,8.587,30.506,8.587,47.68v14.914H597.901
|
||||
c1.507,9.34,4.52,18.002,9.039,25.985c4.52,7.984,10.168,14.914,16.947,20.789c6.779,5.876,14.462,10.471,23.049,13.784
|
||||
c8.587,3.314,17.7,4.972,27.342,4.972c27.418,0,49.563-11.299,66.435-33.896l32.991,24.404
|
||||
c-11.449,15.366-25.609,27.418-42.481,36.155c-16.873,8.737-35.854,13.106-56.944,13.106c-17.174,0-33.217-3.014-48.131-9.039
|
||||
s-27.869-14.462-38.866-25.309s-19.659-23.576-25.986-38.188s-9.491-30.506-9.491-47.679
|
||||
C551.803,184.842,554.817,169.476,560.842,155.014z M624.339,137.162c-12.805,10.696-21.316,24.932-25.534,42.708h140.552
|
||||
c-3.917-17.776-12.278-32.012-25.083-42.708c-12.805-10.695-27.794-16.043-44.967-16.043
|
||||
C652.133,121.119,637.144,126.467,624.339,137.162z"
|
||||
/>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M870.866,142.359c-9.641,10.545-14.462,24.856-14.462,42.934v131.062h-45.646V85.868h45.193v28.472
|
||||
c5.725-9.34,13.182-16.722,22.371-22.145c9.189-5.424,20.111-8.136,32.766-8.136h15.817v42.482h-18.981
|
||||
C892.86,126.542,880.507,131.814,870.866,142.359z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const Logo2: React.FunctionComponent<Props> = ({ isLightTheme, className }) => {
|
||||
const fill = isLightTheme ? '#EA0B8C' : 'white'
|
||||
return (
|
||||
<svg
|
||||
width="49px"
|
||||
height="47.7px"
|
||||
viewBox="0 0 65.8 141"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<g transform="translate(-177.000000, -79.000000)" fill={fill}>
|
||||
<path d="M112.746,82.343 L142.806,82.343 L142.806,163.017 C142.806,175.784 148.646,183.389 153.264,186.649 C148.375,190.995 133.435,194.798 122.298,185.562 C115.732,180.118 112.746,171.166 112.746,162.745 L112.746,82.343 Z M300.609,149.357 L300.609,140.879 L309.777,140.879 L309.777,111.136 L299.709,111.136 C295.844,93.279 279.946,79.898 260.933,79.898 C239.03,79.898 221.275,97.654 221.275,119.556 L221.275,189.015 C227.515,189.892 234.949,188.906 241.274,183.661 C247.839,178.216 250.825,169.265 250.825,160.844 L250.825,158.285 L265.838,158.285 L265.838,128.542 L250.825,128.542 L250.825,119.556 L250.861,119.556 C250.861,113.994 255.37,109.485 260.933,109.485 C266.495,109.485 271.022,113.994 271.022,119.556 L271.022,149.357 C271.022,171.259 288.796,189.015 310.698,189.015 L310.698,159.428 C305.136,159.428 300.609,154.919 300.609,149.357 Z M186.761,111.136 L186.761,154.787 C186.761,157.265 184.705,159.274 182.168,159.274 C179.631,159.274 177.575,157.265 177.575,154.787 L177.575,111.136 L147.831,111.136 L147.831,162.474 C147.831,171.709 150.963,183.389 165.216,187.192 C179.484,190.999 187.761,183.118 187.761,183.118 C187.007,188.31 182.117,192.113 174.24,192.928 C168.28,193.544 160.658,191.57 156.856,189.94 L156.856,217.139 C166.544,219.996 176.791,220.919 186.819,218.973 C205.018,215.442 216.504,200.231 216.504,179.994 L216.504,111.136 L186.761,111.136 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const Logo3: React.FunctionComponent<Props> = ({ isLightTheme, className }) => {
|
||||
// From https://www.yelp.com/brand: "If you need to display the Yelp logo on a white background
|
||||
// be sure to use the version with the grey stroke."
|
||||
const outlineColor = isLightTheme ? '#cccccc' : 'transparent'
|
||||
return (
|
||||
<svg width="90px" height="56px" viewBox="43 59 248 130" version="1.1" className={className}>
|
||||
<defs id="defs6" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,213.33333)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g fillRule="nonzero" stroke="none" fillOpacity="1">
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 2152.1,634.496 c 12.27,25.715 12.86,55.481 1.61,81.672 -11.25,26.199 -33.26,46.281 -60.37,55.094 l -3.22,1.047 c 0,0 45.25,9.554 65.46,53.781 11.85,25.941 12.66,55.598 1.31,81.785 -19.3,44.551 -47.58,83.449 -84.04,115.615 -18.07,15.94 -41.29,24.62 -65.37,24.72 -0.16,0 -0.33,0 -0.48,0 -58.28,0 -83.56,-46.48 -83.56,-46.48 v 161.4 c 0,54.58 -43.57,98.69 -98.87,99 -0.2,0 -0.39,0 -0.58,0 -23.47,0 -66.74,-7.81 -95.43,-15.5 -28.9,-7.74 -57.16,-18.15 -83.98,-30.94 -24.79,-11.83 -43.69,-33.67 -51.84,-59.92 -8.17,-26.3 -4.95,-55.05 8.81,-78.9 l 80.69,-139.765 c 0,0 -19.39,11.524 -46.44,11.524 -42.58,0 -80.28,-27.082 -93.82,-67.379 -1,-2.965 -1.95,-5.941 -2.86,-8.922 -12.87,12.391 -28.44,23.516 -47.4,32.524 -25.81,12.257 -54.7,18.738 -83.56,18.738 -22.03,0 -43.58,-3.738 -63.62,-10.942 -14.91,7.86 -31.86,12.239 -49.69,12.239 -25.28,0 -48.51,-8.867 -66.79,-23.633 v 19.492 c 0,58.711 -47.77,106.474 -106.48,106.474 -58.71,0 -106.47,-47.763 -106.47,-106.474 V 861.98 c -40.122,42.02 -93.606,59.27 -141.887,59.27 -36.2,0 -84.336,-11.781 -124.825,-45.094 -13.57,18.965 -32.839,32.942 -55.503,39.953 -10.329,3.196 -21.028,4.817 -31.805,4.817 -47.399,0 -88.66,-30.434 -102.676,-75.731 l -4.527,-14.625 -14.633,29.985 c -14.668,32.023 -52.141,60.629 -96.383,60.633 -15.211,0 -31.219,-3.379 -47.379,-11.165 -25.875,-12.46 -45.168,-34.484 -54.508,-61.636 -9.343,-27.157 -7.55,-56.321 5.043,-82.125 L 447.941,557.441 c -30.183,-11.793 -52.617,-36.07 -62,-68.992 -9.023,-31.668 -3.308,-64.629 15.688,-90.429 14.914,-20.258 44.305,-44.7 99.605,-46.032 2.008,-0.047 3.985,-0.078 5.957,-0.078 77.582,0.012 134.344,38.371 155.735,105.25 8.801,27.52 19,59.899 29.461,93.36 1.91,-4.258 3.918,-8.45 6.062,-12.52 33.598,-63.68 94.957,-98.762 172.774,-98.762 73.343,0 124.808,24.762 156.627,56.973 18.02,-33.32 53.27,-56 93.73,-56 25.28,0 48.51,8.859 66.79,23.629 v -19.488 c 0,-58.723 47.77,-106.481 106.48,-106.481 57.78,0 104.96,46.258 106.44,103.688 0.07,0 1.75,-0.09 4.72,-0.09 18.4,0 86.61,3.5 133.77,53.781 5.65,6.02 15.7,16.422 25.97,33.75 13.04,-7.18 28.03,-11.27 43.97,-11.27 12.91,0 25.18,2.692 36.31,7.52 6.13,-7.82 13.45,-14.77 21.81,-20.512 39.45,-27.097 85.17,-45.597 132.22,-53.5 5.43,-0.91 10.97,-1.367 16.45,-1.367 53.6,0 97.04,41.969 98.91,95.551 l 0.12,3.418 1.81,-2.899 c 18.2,-29.14 49.6,-46.543 83.99,-46.543 22.92,0 45.3,8.051 63,22.661 36.79,30.363 67.14,69.242 87.76,112.437"
|
||||
/>
|
||||
<path
|
||||
fill={outlineColor}
|
||||
d="m 2001.34,499.398 c -34.39,0 -65.79,17.403 -83.99,46.543 l -1.81,2.899 -0.12,-3.418 c -1.87,-53.582 -45.31,-95.551 -98.91,-95.551 -5.48,0 -11.02,0.457 -16.45,1.367 -47.05,7.903 -92.77,26.403 -132.22,53.5 -8.36,5.742 -15.68,12.692 -21.81,20.512 -11.13,-4.828 -23.4,-7.52 -36.31,-7.52 -15.94,0 -30.93,4.09 -43.97,11.27 -10.27,-17.328 -20.32,-27.73 -25.97,-33.75 -47.16,-50.281 -115.37,-53.781 -133.77,-53.781 -2.97,0 -4.65,0.09 -4.72,0.09 -1.48,-57.43 -48.66,-103.688 -106.44,-103.688 -58.71,0 -106.48,47.758 -106.48,106.481 v 19.488 c -18.28,-14.77 -41.51,-23.629 -66.79,-23.629 -40.46,0 -75.71,22.68 -93.73,56 C 996.031,464 944.566,439.238 871.223,439.238 c -77.817,0 -139.176,35.082 -172.774,98.762 -2.144,4.07 -4.152,8.262 -6.062,12.52 -10.461,-33.461 -20.66,-65.84 -29.461,-93.36 -21.391,-66.879 -78.153,-105.238 -155.735,-105.25 -1.972,0 -3.949,0.031 -5.957,0.078 -55.3,1.332 -84.691,25.774 -99.605,46.032 -18.996,25.8 -24.711,58.761 -15.688,90.429 9.383,32.922 31.817,57.199 62,68.992 L 346.027,766.262 c -12.593,25.804 -14.386,54.968 -5.043,82.125 9.34,27.152 28.633,49.176 54.508,61.636 16.16,7.786 32.168,11.165 47.379,11.165 44.242,-0.004 81.715,-28.61 96.383,-60.633 l 14.633,-29.985 4.527,14.625 c 14.016,45.297 55.277,75.731 102.676,75.731 10.777,0 21.476,-1.621 31.805,-4.817 22.664,-7.011 41.933,-20.988 55.503,-39.953 40.489,33.313 88.625,45.094 124.825,45.094 48.281,0 101.765,-17.25 141.887,-59.27 v 58.766 c 0,58.711 47.76,106.474 106.47,106.474 58.71,0 106.48,-47.763 106.48,-106.474 v -19.492 c 18.28,14.766 41.51,23.633 66.79,23.633 17.83,0 34.78,-4.379 49.69,-12.239 20.04,7.204 41.59,10.942 63.62,10.942 28.86,0 57.75,-6.481 83.56,-18.738 18.96,-9.008 34.53,-20.133 47.4,-32.524 0.91,2.981 1.86,5.957 2.86,8.922 13.54,40.297 51.24,67.379 93.82,67.379 27.05,0 46.44,-11.524 46.44,-11.524 l -80.69,139.765 c -13.76,23.85 -16.98,52.6 -8.81,78.9 8.15,26.25 27.05,48.09 51.84,59.92 26.82,12.79 55.08,23.2 83.98,30.94 28.69,7.69 71.96,15.5 95.43,15.5 0.19,0 0.38,0 0.58,0 55.3,-0.31 98.87,-44.42 98.87,-99 v -161.4 c 0,0 25.28,46.48 83.56,46.48 0.15,0 0.32,0 0.48,0 24.08,-0.1 47.3,-8.78 65.37,-24.72 36.46,-32.166 64.74,-71.064 84.04,-115.615 11.35,-26.187 10.54,-55.844 -1.31,-81.785 -20.21,-44.227 -65.46,-53.781 -65.46,-53.781 l 3.22,-1.047 c 27.11,-8.813 49.12,-28.895 60.37,-55.094 11.25,-26.191 10.66,-55.957 -1.61,-81.672 -20.62,-43.195 -50.97,-82.074 -87.76,-112.437 -17.7,-14.61 -40.08,-22.661 -63,-22.661 z m 104.06,272.645 c 15.79,6.582 40.81,21.348 54.79,51.937 12.59,27.547 13.08,58.86 1.36,85.911 -19.6,45.242 -48.32,84.742 -85.35,117.409 -18.87,16.65 -43.27,25.88 -68.7,25.99 h -0.5 c -40.73,0 -65.97,-21.29 -78.48,-35.86 v 145.7 c 0,27.8 -10.72,53.88 -30.18,73.43 -19.51,19.61 -45.7,30.49 -73.75,30.65 h -0.6 c -23.94,0 -67.99,-7.97 -96.74,-15.67 -29.2,-7.83 -57.75,-18.35 -84.86,-31.27 -26.06,-12.43 -45.93,-35.39 -54.5,-63 -8.58,-27.64 -5.21,-57.87 9.27,-82.93 l 72.98,-126.426 c -8.61,2.945 -20.5,5.789 -34.34,5.789 -44.76,0 -84.4,-28.465 -98.63,-70.836 -0.16,-0.469 -0.32,-0.941 -0.48,-1.418 -12.64,11.086 -27,20.485 -42.79,27.985 -26.49,12.582 -56.14,19.23 -85.74,19.23 -21.85,0 -43.11,-3.531 -63.24,-10.5 -15.43,7.727 -32.69,11.801 -50.07,11.801 -21.94,0 -43.5,-6.582 -61.71,-18.699 v 9.48 c 0,61.512 -50.05,111.554 -111.56,111.554 -61.51,0 -111.55,-50.042 -111.55,-111.554 v -46.68 c -36.296,33.348 -85.467,52.258 -136.807,52.258 -21.012,0 -74.321,-4.261 -123.91,-42.937 -14.075,17.836 -32.954,30.773 -54.918,37.57 -10.817,3.348 -22.024,5.047 -33.305,5.047 -49.637,0 -92.848,-31.871 -107.527,-79.309 l -0.739,-2.382 -9.011,18.468 c -13.973,30.516 -51.559,63.481 -100.942,63.485 -16.824,0 -33.508,-3.926 -49.582,-11.668 -26.973,-12.993 -47.254,-35.922 -57.105,-64.563 -9.786,-28.433 -7.911,-58.976 5.281,-86 l 99.558,-203.996 c -29.582,-13.23 -50.777,-37.961 -59.964,-70.199 -9.457,-33.192 -3.454,-67.75 16.48,-94.828 15.59,-21.18 46.215,-46.711 103.57,-48.102 2.047,-0.051 4.071,-0.07 6.082,-0.07 79.938,0 138.461,39.648 160.567,108.769 7.168,22.411 15.773,49.653 25.633,81.121 0.187,-0.371 0.379,-0.73 0.57,-1.089 34.516,-65.442 97.469,-101.481 177.262,-101.481 79.859,0 128.156,28.281 155.507,53.871 20.31,-32.793 56.07,-52.902 94.85,-52.902 21.94,0 43.5,6.582 61.71,18.703 v -9.48 c 0,-61.512 50.05,-111.563 111.56,-111.563 29.24,0 56.9,11.281 77.88,31.762 19.72,19.258 31.47,44.609 33.39,71.847 20.21,0.012 89.25,4.09 137.36,55.383 l 0.76,0.801 c 5.41,5.758 14.16,15.066 23.38,29.738 13.05,-6.34 27.51,-9.672 42.1,-9.672 11.99,0 23.67,2.184 34.75,6.473 6.01,-7.109 12.89,-13.34 20.5,-18.57 40.05,-27.512 86.47,-46.289 134.25,-54.321 5.71,-0.949 11.52,-1.441 17.29,-1.441 52.55,0 95.81,38.371 103,89.242 19.63,-25.051 49.47,-39.711 81.83,-39.711 24.1,0 47.62,8.461 66.23,23.821 37.36,30.828 68.18,70.308 89.11,114.168 12.9,27.035 13.52,58.328 1.7,85.859 -10.34,24.078 -29.32,43.242 -52.98,53.875"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
stroke="none"
|
||||
fillOpacity="1"
|
||||
fill="#050505"
|
||||
d="m 1159.17,546.68 c 0,-20.758 -16.83,-37.59 -37.59,-37.59 -20.76,0 -37.58,16.832 -37.58,37.59 v 374.066 c 0,20.762 16.82,37.59 37.58,37.59 20.76,0 37.59,-16.828 37.59,-37.59 V 546.68"
|
||||
/>
|
||||
<g fillRule="nonzero" stroke="none" fillOpacity="1" fill="#050505">
|
||||
<path d="m 817.418,718.516 c 2.512,51.925 41.84,60.464 59.527,60.464 17.782,0 57.258,-8.648 59.36,-61.347 0,-2.789 -1.492,-4.434 -3.141,-4.434 H 820.688 c -1.817,0 -3.454,1.832 -3.27,5.317 z m 16.348,-67.184 135.035,-0.141 c 0.933,0 1.863,0.071 2.793,0.137 18.734,1.418 36.496,16.961 36.566,41.856 0,0.046 0.01,0.093 0.01,0.14 -0.02,0.656 -0.09,1.262 -0.13,1.906 -6.7,137.254 -94.841,157.129 -134.817,157.129 -41.567,0 -135.7,-24.535 -135.7,-177.152 0.715,-57.391 13.008,-167.078 133.7,-167.078 104.254,0 127.593,60.211 127.593,75.57 0,22.656 -18.871,41.875 -40.183,34.645 -17.297,-5.867 -41.016,-40.313 -79.238,-40.313 -54.688,0 -61.77,50.004 -61.77,60.653 0,8.789 4.586,12.211 16.141,12.648" />
|
||||
<path d="M 525.379,555.84 407.938,796.477 c -9.356,19.164 -1.403,42.285 17.765,51.636 19.164,9.36 42.285,1.403 51.637,-17.765 l 90.926,-186.317 55.957,180.797 c 6.304,20.371 27.918,31.781 48.304,25.473 20.371,-6.305 31.774,-27.934 25.469,-48.305 0,0 -62.891,-205.676 -100.687,-323.855 -12.504,-39.102 -45.274,-58.493 -94.387,-57.282 -48.578,1.172 -56.113,29.821 -50.731,48.711 5.922,20.77 23.262,27.051 40.235,26.961 43.887,-0.23 49.476,26.36 32.953,59.309" />
|
||||
<path d="m 1605.27,606.609 h 4.16 c 3.38,0 6.53,0.176 6.53,3.993 0,3.363 -2.87,3.765 -5.48,3.765 h -5.21 z m -3.39,10.793 h 8.9 c 5.79,0 8.53,-2.136 8.53,-6.98 0,-4.32 -2.78,-6.168 -6.41,-6.563 l 6.99,-10.711 h -3.66 l -6.62,10.438 h -4.34 v -10.438 h -3.39 z m 7.83,-29.843 c 9.84,0 17.72,7.722 17.72,17.972 0,10.043 -7.88,17.77 -17.72,17.77 -9.84,0 -17.72,-7.727 -17.72,-17.985 0,-10.035 7.88,-17.757 17.72,-17.757 z m 0,38.777 c 11.47,0 21.09,-8.992 21.09,-20.805 0,-12.019 -9.62,-21.019 -21.09,-21.019 -11.47,0 -21.07,9 -21.07,20.804 0,12.028 9.6,21.02 21.07,21.02" />
|
||||
<path d="m 1456.07,667.563 c 0,-29.711 0,-83.051 -54.02,-83.051 -54.18,0 -54.02,53.34 -54.02,83.051 v 27.14 c 0,29.711 0,83.051 54.02,83.051 54.17,0 54.02,-53.34 54.02,-83.051 z m 74.45,28.542 c 0,49.329 -6.06,116.926 -68.35,146.516 -35.14,16.688 -78.42,16.395 -111.7,-2.84 -6.67,-3.859 -13.28,-8.129 -19.13,-12.496 -4,16.469 -18.8,28.715 -36.49,28.715 -20.76,0 -37.59,-16.828 -37.59,-37.59 V 444.352 c 0,-20.762 16.83,-37.59 37.59,-37.59 20.76,0 37.59,16.828 37.59,37.59 v 98.168 c 0,0 25.25,-30.961 65.45,-32.008 142.76,-3.723 133.41,151.551 132.63,185.593" />
|
||||
</g>
|
||||
<g fillRule="evenodd" stroke="none" fillOpacity="1" fill="#d32323">
|
||||
<path d="m 1648.99,876.68 131.26,-64.012 c 25.25,-12.313 21.34,-49.445 -5.91,-56.238 l -141.69,-35.332 c -17.68,-4.407 -35.13,7.789 -37.21,25.882 -4.46,38.704 -0.06,76.93 11.84,112.317 5.77,17.187 25.41,25.332 41.71,17.383" />
|
||||
<path d="m 1701.54,606.473 97.7,108.511 c 18.79,20.879 53.41,6.895 52.43,-21.179 l -5.1,-145.985 c -0.63,-18.191 -17.15,-31.66 -35.1,-28.64 -37.75,6.34 -73.46,20.929 -104.63,42.34 -14.98,10.289 -17.46,31.441 -5.3,44.953" />
|
||||
<path d="m 1933.18,750.875 138.87,-45.129 c 17.28,-5.617 25.71,-25.176 17.88,-41.578 -16.29,-34.125 -39.92,-64.617 -69.44,-88.977 -14.04,-11.589 -35.07,-8.191 -44.72,7.25 l -77.41,123.875 c -14.88,23.825 8.1,53.243 34.82,44.559" />
|
||||
<path d="m 2074.4,839.465 -140.38,-40.25 c -27,-7.742 -48.94,22.461 -33.23,45.75 l 81.67,121.066 c 10.14,15.032 31.21,17.797 44.81,5.805 27.99,-24.699 50.92,-55.602 66.41,-91.352 7.24,-16.711 -1.77,-36 -19.28,-41.019" />
|
||||
<path d="m 1746.39,1180.09 c -25.26,-6.77 -49.36,-15.71 -72.15,-26.58 -15.82,-7.55 -21.79,-27.02 -13.03,-42.19 l 137.2,-237.64 c 15.39,-26.66 56.14,-15.742 56.14,15.043 v 274.397 c 0,17.52 -14.91,31.4 -32.38,30.03 -25.17,-1.99 -50.51,-6.29 -75.78,-13.06" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
16
web/src/enterprise/dotcom/welcome/routes.tsx
Normal file
16
web/src/enterprise/dotcom/welcome/routes.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { WelcomeAreaRoute } from './WelcomeArea'
|
||||
const WelcomeMainPage = React.lazy(async () => ({
|
||||
default: (await import('./WelcomeMainPage')).WelcomeMainPage,
|
||||
}))
|
||||
|
||||
export const welcomeAreaRoutes: ReadonlyArray<WelcomeAreaRoute> = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
// tslint:disable-next-line:jsx-no-lambda
|
||||
render: props => <WelcomeMainPage {...props} />,
|
||||
},
|
||||
// We will add more pages here soon. The other pages (search, code intel, integrations) were
|
||||
// removed to avoid blocking shipping of the new main welcome page.
|
||||
]
|
||||
@ -1,5 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Redirect } from 'react-router'
|
||||
import { LayoutRouteProps, routes } from '../routes'
|
||||
import { welcomeAreaRoutes } from './dotcom/welcome/routes'
|
||||
const WelcomeArea = React.lazy(async () => ({
|
||||
default: (await import('./dotcom/welcome/WelcomeArea')).WelcomeArea,
|
||||
}))
|
||||
const NewProductSubscriptionPageOrRedirectUser = React.lazy(async () => ({
|
||||
default: (await import('./user/productSubscriptions/NewProductSubscriptionPageOrRedirectUser'))
|
||||
.NewProductSubscriptionPageOrRedirectUser,
|
||||
@ -13,5 +18,14 @@ export const enterpriseRoutes: ReadonlyArray<LayoutRouteProps> = [
|
||||
exact: true,
|
||||
render: props => <NewProductSubscriptionPageOrRedirectUser {...props} />,
|
||||
},
|
||||
{
|
||||
path: '/start',
|
||||
render: () => <Redirect to="/welcome" />,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/welcome',
|
||||
render: props => <WelcomeArea {...props} routes={welcomeAreaRoutes} />,
|
||||
},
|
||||
...routes,
|
||||
]
|
||||
|
||||
@ -22,19 +22,26 @@
|
||||
}
|
||||
|
||||
&__logo {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
&:hover {
|
||||
animation: spin 0.5s ease-in-out 1;
|
||||
&--full {
|
||||
// stylelint-disable-next-line declaration-property-unit-whitelist
|
||||
width: 150px;
|
||||
margin-right: 0.25rem; // full logo image has slightly more spacing on left side
|
||||
}
|
||||
&:not(&--full) {
|
||||
width: 1.5rem;
|
||||
&:hover {
|
||||
animation: spin 0.5s ease-in-out 1;
|
||||
|
||||
@keyframes spin {
|
||||
50% {
|
||||
transform: rotate(180deg) scale(1.2);
|
||||
}
|
||||
@keyframes spin {
|
||||
50% {
|
||||
transform: rotate(180deg) scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(180deg) scale(1);
|
||||
100% {
|
||||
transform: rotate(180deg) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { authRequired } from '../auth'
|
||||
import { KeybindingsProps } from '../keybindings'
|
||||
import { parseSearchURLQuery } from '../search'
|
||||
import { SearchNavbarItem } from '../search/input/SearchNavbarItem'
|
||||
import { showDotComMarketing } from '../util/features'
|
||||
import { NavLinks } from './NavLinks'
|
||||
|
||||
interface Props extends SettingsCascadeProps, PlatformContextProps, ExtensionsControllerProps, KeybindingsProps {
|
||||
@ -70,7 +71,19 @@ export class GlobalNavbar extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
const logo = <img className="global-navbar__logo" src="/.assets/img/sourcegraph-mark.svg" />
|
||||
let logoSrc: string
|
||||
const showFullLogo = this.props.location.pathname === '/welcome'
|
||||
if (showFullLogo) {
|
||||
logoSrc = this.props.isLightTheme
|
||||
? '/.assets/img/sourcegraph-light-head-logo.svg'
|
||||
: '/.assets/img/sourcegraph-head-logo.svg'
|
||||
} else {
|
||||
logoSrc = '/.assets/img/sourcegraph-mark.svg'
|
||||
}
|
||||
|
||||
const logo = (
|
||||
<img className={`global-navbar__logo ${showFullLogo ? 'global-navbar__logo--full' : ''}`} src={logoSrc} />
|
||||
)
|
||||
return (
|
||||
<div className={`global-navbar ${this.props.lowProfile ? '' : 'global-navbar--bg'}`}>
|
||||
{this.props.lowProfile ? (
|
||||
@ -84,8 +97,8 @@ export class GlobalNavbar extends React.PureComponent<Props, State> {
|
||||
{logo}
|
||||
</Link>
|
||||
)}
|
||||
{!this.state.authRequired && (
|
||||
<div className="global-navbar__search-box-container">
|
||||
{!this.state.authRequired && this.props.location.pathname !== '/welcome' && (
|
||||
<div className="global-navbar__search-box-container d-none d-sm-flex">
|
||||
<SearchNavbarItem
|
||||
{...this.props}
|
||||
navbarSearchQuery={this.props.navbarSearchQuery}
|
||||
@ -95,7 +108,7 @@ export class GlobalNavbar extends React.PureComponent<Props, State> {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!this.state.authRequired && <NavLinks {...this.props} />}
|
||||
{!this.state.authRequired && <NavLinks {...this.props} showDotComMarketing={showDotComMarketing} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
84
web/src/nav/NavLinks.test.tsx
Normal file
84
web/src/nav/NavLinks.test.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import * as H from 'history'
|
||||
import { flatten } from 'lodash'
|
||||
import React from 'react'
|
||||
import { createRenderer } from 'react-test-renderer/shallow'
|
||||
import { setLinkComponent } from '../../../shared/src/components/Link'
|
||||
import { ExtensionsControllerProps } from '../../../shared/src/extensions/controller'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { SettingsCascadeProps } from '../../../shared/src/settings/settings'
|
||||
import { KeybindingsProps } from '../keybindings'
|
||||
import { NavLinks } from './NavLinks'
|
||||
|
||||
// Renders a human-readable list of the NavLinks' contents so that humans can more easily diff
|
||||
// snapshots to see what actually changed.
|
||||
const renderShallow = (element: React.ReactElement<NavLinks['props']>): any => {
|
||||
const renderer = createRenderer()
|
||||
renderer.render(element)
|
||||
|
||||
const getDisplayName = (element: React.ReactChild): string | string[] => {
|
||||
if (element === null) {
|
||||
return []
|
||||
} else if (typeof element === 'string' || typeof element === 'number') {
|
||||
return element.toString()
|
||||
} else if (element.type === 'li' && (element.props.children.props.href || element.props.children.props.to)) {
|
||||
return `${element.props.children.props.children} ${element.props.children.props.href ||
|
||||
element.props.children.props.to}`
|
||||
} else if (typeof element.type === 'symbol' || typeof element.type === 'string') {
|
||||
return flatten(React.Children.map(element.props.children, element => getDisplayName(element)))
|
||||
} else {
|
||||
return element.type.displayName || element.type.name || 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
return flatten(
|
||||
React.Children.map(renderer.getRenderOutput().props.children, e => getDisplayName(e)).filter(e => !!e)
|
||||
)
|
||||
}
|
||||
|
||||
describe('NavLinks', () => {
|
||||
setLinkComponent((props: any) => <a {...props} />)
|
||||
afterAll(() => setLinkComponent(null as any)) // reset global env for other tests
|
||||
const NOOP_EXTENSIONS_CONTROLLER: ExtensionsControllerProps<
|
||||
'executeCommand' | 'services'
|
||||
>['extensionsController'] = { executeCommand: async () => void 0, services: {} as any }
|
||||
const NOOP_PLATFORM_CONTEXT = { forceUpdateTooltip: () => void 0 }
|
||||
const KEYBINDINGS: KeybindingsProps['keybindings'] = { commandPalette: [] }
|
||||
const SETTINGS_CASCADE: SettingsCascadeProps['settingsCascade'] = { final: null, subjects: null }
|
||||
// tslint:disable-next-line:no-object-literal-type-assertion
|
||||
const USER = { username: 'u' } as GQL.IUser
|
||||
const history = H.createMemoryHistory({ keyLength: 0 })
|
||||
const commonProps = {
|
||||
extensionsController: NOOP_EXTENSIONS_CONTROLLER,
|
||||
platformContext: NOOP_PLATFORM_CONTEXT,
|
||||
isLightTheme: true,
|
||||
onThemeChange: () => void 0,
|
||||
keybindings: KEYBINDINGS,
|
||||
settingsCascade: SETTINGS_CASCADE,
|
||||
}
|
||||
|
||||
// The 3 main props that affect the desired contents of NavLinks are whether the user is signed
|
||||
// in, whether we're on Sourcegraph.com, and the path. Create snapshots of all permutations.
|
||||
for (const authenticatedUser of [null, USER]) {
|
||||
for (const showDotComMarketing of [false, true]) {
|
||||
for (const path of ['/foo', '/search', '/welcome']) {
|
||||
const name = [
|
||||
authenticatedUser ? 'authed' : 'unauthed',
|
||||
showDotComMarketing ? 'Sourcegraph.com' : 'self-hosted',
|
||||
path,
|
||||
].join(' ')
|
||||
test(name, () => {
|
||||
expect(
|
||||
renderShallow(
|
||||
<NavLinks
|
||||
{...commonProps}
|
||||
authenticatedUser={authenticatedUser}
|
||||
showDotComMarketing={showDotComMarketing}
|
||||
location={H.createLocation(path, history.location)}
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,27 +1,28 @@
|
||||
import * as H from 'history'
|
||||
import * as React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ActionsNavItems } from '../../../shared/src/actions/ActionsNavItems'
|
||||
import { ContributableMenu } from '../../../shared/src/api/protocol'
|
||||
import { CommandListPopoverButton } from '../../../shared/src/commandPalette/CommandList'
|
||||
import { Link } from '../../../shared/src/components/Link'
|
||||
import { ExtensionsControllerProps } from '../../../shared/src/extensions/controller'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { PlatformContextProps } from '../../../shared/src/platform/context'
|
||||
import { SettingsCascadeProps } from '../../../shared/src/settings/settings'
|
||||
import { isDiscussionsEnabled } from '../discussions'
|
||||
import { KeybindingsProps } from '../keybindings'
|
||||
import { eventLogger } from '../tracking/eventLogger'
|
||||
import { showDotComMarketing } from '../util/features'
|
||||
import { UserNavItem } from './UserNavItem'
|
||||
|
||||
interface Props extends SettingsCascadeProps, PlatformContextProps, ExtensionsControllerProps, KeybindingsProps {
|
||||
interface Props
|
||||
extends SettingsCascadeProps,
|
||||
KeybindingsProps,
|
||||
ExtensionsControllerProps<'executeCommand' | 'services'>,
|
||||
PlatformContextProps<'forceUpdateTooltip'> {
|
||||
location: H.Location
|
||||
history: H.History
|
||||
authenticatedUser: GQL.IUser | null
|
||||
isLightTheme: boolean
|
||||
onThemeChange: () => void
|
||||
isMainPage?: boolean
|
||||
showDotComMarketing: boolean
|
||||
}
|
||||
|
||||
export class NavLinks extends React.PureComponent<Props> {
|
||||
@ -31,24 +32,28 @@ export class NavLinks extends React.PureComponent<Props> {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
|
||||
private onClickInstall = (): void => {
|
||||
eventLogger.log('InstallSourcegraphServerCTAClicked', {
|
||||
location_on_page: 'Navbar',
|
||||
})
|
||||
}
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
return (
|
||||
<ul className="nav-links nav align-items-center pl-2 pr-1">
|
||||
{showDotComMarketing && (
|
||||
{/* Show "Search" link on small screens when GlobalNavbar hides the SearchNavbarItem. */}
|
||||
{this.props.location.pathname !== '/search' && this.props.location.pathname !== '/welcome' && (
|
||||
<li className="nav-item d-sm-none">
|
||||
<Link className="nav-link" to="/search">
|
||||
Search
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{this.props.showDotComMarketing && this.props.location.pathname !== '/welcome' && (
|
||||
<li className="nav-item">
|
||||
<a
|
||||
href="https://docs.sourcegraph.com/#quickstart"
|
||||
className="nav-link text-truncate"
|
||||
onClick={this.onClickInstall}
|
||||
title="Install self-hosted Sourcegraph to search your own (private) code"
|
||||
>
|
||||
Install Sourcegraph
|
||||
<Link to="/welcome" className="nav-link">
|
||||
Welcome
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{this.props.showDotComMarketing && this.props.location.pathname === '/welcome' && (
|
||||
<li className="nav-item">
|
||||
<a href="https://docs.sourcegraph.com" className="nav-link" target="_blank">
|
||||
Docs
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
@ -59,18 +64,24 @@ export class NavLinks extends React.PureComponent<Props> {
|
||||
platformContext={this.props.platformContext}
|
||||
location={this.props.location}
|
||||
/>
|
||||
<li className="nav-item">
|
||||
<Link to="/explore" className="nav-link">
|
||||
Explore
|
||||
</Link>
|
||||
</li>
|
||||
{(!this.props.showDotComMarketing ||
|
||||
!!this.props.authenticatedUser ||
|
||||
this.props.location.pathname !== '/welcome') && (
|
||||
<li className="nav-item">
|
||||
<Link to="/explore" className="nav-link">
|
||||
Explore
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{!this.props.authenticatedUser && (
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<Link to="/extensions" className="nav-link">
|
||||
Extensions
|
||||
</Link>
|
||||
</li>
|
||||
{this.props.location.pathname !== '/welcome' && (
|
||||
<li className="nav-item">
|
||||
<Link to="/extensions" className="nav-link">
|
||||
Extensions
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{this.props.location.pathname !== '/sign-in' && (
|
||||
<li className="nav-item mx-1">
|
||||
<Link className="nav-link btn btn-primary" to="/sign-in">
|
||||
@ -78,33 +89,37 @@ export class NavLinks extends React.PureComponent<Props> {
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{showDotComMarketing && (
|
||||
{this.props.showDotComMarketing && (
|
||||
<li className="nav-item">
|
||||
<a href="https://about.sourcegraph.com" className="nav-link">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li className="nav-item">
|
||||
<Link to="/help" className="nav-link">
|
||||
Help
|
||||
</Link>
|
||||
</li>
|
||||
{this.props.location.pathname !== '/welcome' && (
|
||||
<li className="nav-item">
|
||||
<Link to="/help" className="nav-link">
|
||||
Help
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CommandListPopoverButton
|
||||
menu={ContributableMenu.CommandPalette}
|
||||
extensionsController={this.props.extensionsController}
|
||||
platformContext={this.props.platformContext}
|
||||
toggleVisibilityKeybinding={this.props.keybindings.commandPalette}
|
||||
location={this.props.location}
|
||||
/>
|
||||
{this.props.location.pathname !== '/welcome' && (
|
||||
<CommandListPopoverButton
|
||||
menu={ContributableMenu.CommandPalette}
|
||||
extensionsController={this.props.extensionsController}
|
||||
platformContext={this.props.platformContext}
|
||||
toggleVisibilityKeybinding={this.props.keybindings.commandPalette}
|
||||
location={this.props.location}
|
||||
/>
|
||||
)}
|
||||
{this.props.authenticatedUser && (
|
||||
<li className="nav-item">
|
||||
<UserNavItem
|
||||
{...this.props}
|
||||
authenticatedUser={this.props.authenticatedUser}
|
||||
showAbout={showDotComMarketing}
|
||||
showAbout={this.props.showDotComMarketing}
|
||||
showDiscussions={isDiscussionsEnabled(this.props.settingsCascade)}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@ -11,7 +11,6 @@ interface Props {
|
||||
authenticatedUser: GQL.IUser
|
||||
isLightTheme: boolean
|
||||
onThemeChange: () => void
|
||||
isMainPage?: boolean
|
||||
showAbout: boolean
|
||||
showDiscussions: boolean
|
||||
}
|
||||
|
||||
125
web/src/nav/__snapshots__/NavLinks.test.tsx.snap
Normal file
125
web/src/nav/__snapshots__/NavLinks.test.tsx.snap
Normal file
@ -0,0 +1,125 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NavLinks authed Sourcegraph.com /foo 1`] = `
|
||||
Array [
|
||||
"Search /search",
|
||||
"Welcome /welcome",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"CommandListPopoverButton",
|
||||
"UserNavItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks authed Sourcegraph.com /search 1`] = `
|
||||
Array [
|
||||
"Welcome /welcome",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"CommandListPopoverButton",
|
||||
"UserNavItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks authed Sourcegraph.com /welcome 1`] = `
|
||||
Array [
|
||||
"Docs https://docs.sourcegraph.com",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"UserNavItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks authed self-hosted /foo 1`] = `
|
||||
Array [
|
||||
"Search /search",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"CommandListPopoverButton",
|
||||
"UserNavItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks authed self-hosted /search 1`] = `
|
||||
Array [
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"CommandListPopoverButton",
|
||||
"UserNavItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks authed self-hosted /welcome 1`] = `
|
||||
Array [
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"UserNavItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks unauthed Sourcegraph.com /foo 1`] = `
|
||||
Array [
|
||||
"Search /search",
|
||||
"Welcome /welcome",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"Extensions /extensions",
|
||||
"Sign in /sign-in",
|
||||
"About https://about.sourcegraph.com",
|
||||
"Help /help",
|
||||
"CommandListPopoverButton",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks unauthed Sourcegraph.com /search 1`] = `
|
||||
Array [
|
||||
"Welcome /welcome",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"Extensions /extensions",
|
||||
"Sign in /sign-in",
|
||||
"About https://about.sourcegraph.com",
|
||||
"Help /help",
|
||||
"CommandListPopoverButton",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks unauthed Sourcegraph.com /welcome 1`] = `
|
||||
Array [
|
||||
"Docs https://docs.sourcegraph.com",
|
||||
"ActionsNavItems",
|
||||
"Sign in /sign-in",
|
||||
"About https://about.sourcegraph.com",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks unauthed self-hosted /foo 1`] = `
|
||||
Array [
|
||||
"Search /search",
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"Extensions /extensions",
|
||||
"Sign in /sign-in",
|
||||
"Help /help",
|
||||
"CommandListPopoverButton",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks unauthed self-hosted /search 1`] = `
|
||||
Array [
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"Extensions /extensions",
|
||||
"Sign in /sign-in",
|
||||
"Help /help",
|
||||
"CommandListPopoverButton",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`NavLinks unauthed self-hosted /welcome 1`] = `
|
||||
Array [
|
||||
"ActionsNavItems",
|
||||
"Explore /explore",
|
||||
"Sign in /sign-in",
|
||||
]
|
||||
`;
|
||||
@ -2,9 +2,6 @@ import * as React from 'react'
|
||||
import { Redirect, RouteComponentProps } from 'react-router'
|
||||
import { LayoutProps } from './Layout'
|
||||
import { parseSearchURLQuery } from './search'
|
||||
const MainPage = React.lazy(async () => ({
|
||||
default: (await import('./search/input/MainPage')).MainPage,
|
||||
}))
|
||||
const SearchPage = React.lazy(async () => ({
|
||||
default: (await import('./search/input/SearchPage')).SearchPage,
|
||||
}))
|
||||
@ -78,12 +75,11 @@ export const routes: ReadonlyArray<LayoutRouteProps> = [
|
||||
{
|
||||
path: '/',
|
||||
render: (props: any) =>
|
||||
window.context.sourcegraphDotComMode && !props.user ? <Redirect to="/start" /> : <Redirect to="/search" />,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/start',
|
||||
render: (props: any) => <MainPage {...props} />,
|
||||
window.context.sourcegraphDotComMode && !props.user ? (
|
||||
<Redirect to="/welcome" />
|
||||
) : (
|
||||
<Redirect to="/search" />
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,353 +0,0 @@
|
||||
import { createHoverifier, findPositionsFromEvents, HoveredToken, HoverState } from '@sourcegraph/codeintellify'
|
||||
import { getTokenAtPosition } from '@sourcegraph/codeintellify/lib/token_position'
|
||||
import { Position } from '@sourcegraph/extension-api-types'
|
||||
import * as H from 'history'
|
||||
import * as React from 'react'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { catchError, filter, map, withLatestFrom } from 'rxjs/operators'
|
||||
import { ActionItemProps } from '../../../../shared/src/actions/ActionItem'
|
||||
import { HoverMerged } from '../../../../shared/src/api/client/types/hover'
|
||||
import { ExtensionsControllerProps } from '../../../../shared/src/extensions/controller'
|
||||
import * as GQL from '../../../../shared/src/graphql/schema'
|
||||
import { getHoverActions } from '../../../../shared/src/hover/actions'
|
||||
import { HoverContext, HoverOverlay } from '../../../../shared/src/hover/HoverOverlay'
|
||||
import { getModeFromPath } from '../../../../shared/src/languages'
|
||||
import { PlatformContextProps } from '../../../../shared/src/platform/context'
|
||||
import { ErrorLike, isErrorLike } from '../../../../shared/src/util/errors'
|
||||
import { isDefined, propertyIsDefined } from '../../../../shared/src/util/types'
|
||||
import { FileSpec, ModeSpec, PositionSpec, RepoSpec, ResolvedRevSpec, RevSpec } from '../../../../shared/src/util/url'
|
||||
import { getHover } from '../../backend/features'
|
||||
import { fetchBlob } from '../../repo/blob/BlobPage'
|
||||
|
||||
interface Props extends ExtensionsControllerProps, PlatformContextProps {
|
||||
history: H.History
|
||||
location: H.Location
|
||||
className: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
parentElement: string
|
||||
|
||||
overlayPortal?: HTMLElement
|
||||
tooltipClass: string
|
||||
defaultHoverPosition: Position
|
||||
}
|
||||
|
||||
interface State extends HoverState<HoverContext, HoverMerged, ActionItemProps> {
|
||||
/**
|
||||
* The blob data or error that happened.
|
||||
* undefined while loading.
|
||||
*/
|
||||
blobOrError?: GQL.IGitBlob | ErrorLike
|
||||
target?: EventTarget
|
||||
}
|
||||
|
||||
const domFunctions = {
|
||||
getCodeElementFromTarget: (target: HTMLElement): HTMLTableCellElement | null => {
|
||||
// If the target is part of the decoration, return null.
|
||||
if (
|
||||
target.classList.contains('line-decoration-attachment') ||
|
||||
target.classList.contains('line-decoration-attachment__contents')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const row = target.closest('tr')
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
return row.cells[1]
|
||||
},
|
||||
getCodeElementFromLineNumber: (codeView: HTMLElement, line: number): HTMLElement | null => {
|
||||
const lineNumberElement = codeView.querySelector(`td[data-line="${line}"]`)
|
||||
|
||||
if (!lineNumberElement) {
|
||||
return null
|
||||
}
|
||||
return lineNumberElement.nextElementSibling as HTMLElement | null
|
||||
},
|
||||
getLineNumberFromCodeElement: (codeCell: HTMLElement): number => {
|
||||
const row = codeCell.closest('tr')
|
||||
if (!row) {
|
||||
throw new Error('Could not find closest row for codeCell')
|
||||
}
|
||||
const numberCell = row.cells[0]
|
||||
if (!numberCell || !numberCell.dataset.line) {
|
||||
throw new Error('Could not find line number')
|
||||
}
|
||||
return parseInt(numberCell.dataset.line, 10)
|
||||
},
|
||||
}
|
||||
|
||||
const REPO_NAME = 'github.com/gorilla/mux'
|
||||
const COMMIT_ID = '9e1f5955c0d22b55d9e20d6faa28589f83b2faca'
|
||||
const REV = undefined
|
||||
const FILE_PATH = 'mux.go'
|
||||
|
||||
export class CodeIntellifyBlob extends React.Component<Props, State> {
|
||||
/** Emits whenever the ref callback for the code element is called */
|
||||
private codeViewElements = new Subject<HTMLElement | null>()
|
||||
private nextCodeViewElement = (element: HTMLElement | null) => this.codeViewElements.next(element)
|
||||
|
||||
/** Emits whenever the ref callback for the hover element is called */
|
||||
private hoverOverlayElements = new Subject<HTMLElement | null>()
|
||||
private nextOverlayElement = (element: HTMLElement | null) => this.hoverOverlayElements.next(element)
|
||||
|
||||
/** Emits whenever the ref callback for the demo file element is called */
|
||||
private codeIntellifyBlobElements = new Subject<HTMLElement | null>()
|
||||
private nextCodeIntellifyBlobElements = (element: HTMLElement | null) =>
|
||||
this.codeIntellifyBlobElements.next(element)
|
||||
|
||||
/** Emits when the close button was clicked */
|
||||
private closeButtonClicks = new Subject<MouseEvent>()
|
||||
private nextCloseButtonClick = (event: MouseEvent) => this.closeButtonClicks.next(event)
|
||||
|
||||
private subscriptions = new Subscription()
|
||||
|
||||
private componentUpdates = new Subject<void>()
|
||||
|
||||
private target: EventTarget | null = null
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
|
||||
const hoverifier = createHoverifier<
|
||||
RepoSpec & RevSpec & FileSpec & ResolvedRevSpec,
|
||||
HoverMerged,
|
||||
ActionItemProps
|
||||
>({
|
||||
closeButtonClicks: this.closeButtonClicks,
|
||||
hoverOverlayElements: this.hoverOverlayElements,
|
||||
hoverOverlayRerenders: this.componentUpdates.pipe(
|
||||
withLatestFrom(this.hoverOverlayElements, this.codeIntellifyBlobElements),
|
||||
map(([, hoverOverlayElement, codeIntellifyBlobElement]) => ({
|
||||
hoverOverlayElement,
|
||||
codeIntellifyBlobElement,
|
||||
})),
|
||||
filter(propertyIsDefined('codeIntellifyBlobElement')),
|
||||
map(({ hoverOverlayElement, codeIntellifyBlobElement }) => ({
|
||||
hoverOverlayElement,
|
||||
relativeElement: codeIntellifyBlobElement.closest(this.props.parentElement) as HTMLElement | null,
|
||||
})),
|
||||
// Can't reposition HoverOverlay or file weren't rendered
|
||||
filter(propertyIsDefined('relativeElement')),
|
||||
filter(propertyIsDefined('hoverOverlayElement'))
|
||||
),
|
||||
getHover: hoveredToken => getHover(this.getLSPTextDocumentPositionParams(hoveredToken), this.props),
|
||||
getActions: context => getHoverActions(this.props, context),
|
||||
})
|
||||
|
||||
this.subscriptions.add(hoverifier)
|
||||
const positionEvents = this.codeViewElements.pipe(
|
||||
filter(isDefined),
|
||||
findPositionsFromEvents(domFunctions)
|
||||
)
|
||||
|
||||
const targets = positionEvents.pipe(map(({ event: { target } }) => target))
|
||||
|
||||
targets.subscribe(target => (this.target = target))
|
||||
|
||||
this.subscriptions.add(
|
||||
hoverifier.hoverify({
|
||||
positionEvents,
|
||||
resolveContext: () => ({
|
||||
repoName: REPO_NAME,
|
||||
commitID: COMMIT_ID,
|
||||
rev: REV || '',
|
||||
filePath: FILE_PATH,
|
||||
}),
|
||||
dom: domFunctions,
|
||||
})
|
||||
)
|
||||
|
||||
this.subscriptions.add(hoverifier.hoverStateUpdates.subscribe(update => this.setState(update)))
|
||||
|
||||
this.subscriptions.add(
|
||||
this.codeViewElements
|
||||
.pipe(
|
||||
filter(isDefined),
|
||||
map(codeView => getTokenAtPosition(codeView, props.defaultHoverPosition, domFunctions)),
|
||||
filter(isDefined)
|
||||
)
|
||||
.subscribe(token => {
|
||||
const showOnHomepage = props.className === 'code-intellify-container' && window.innerWidth >= 1393
|
||||
const showOnModal =
|
||||
props.className === 'code-intellify-container-modal' && window.innerWidth >= 1275
|
||||
if (showOnHomepage || showOnModal) {
|
||||
token.click()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
// Fetch repository revision.
|
||||
fetchBlob({
|
||||
repoName: REPO_NAME,
|
||||
commitID: COMMIT_ID,
|
||||
filePath: FILE_PATH,
|
||||
isLightTheme: false,
|
||||
disableTimeout: false,
|
||||
})
|
||||
.pipe(
|
||||
catchError(error => {
|
||||
console.error(error)
|
||||
return [error]
|
||||
})
|
||||
)
|
||||
.subscribe(blobOrError => this.setState({ blobOrError }), err => console.error(err))
|
||||
|
||||
this.componentUpdates.next()
|
||||
|
||||
this.subscriptions.add(
|
||||
this.props.extensionsController.services.model.model.next({
|
||||
...this.props.extensionsController.services.model.model.value,
|
||||
visibleViewComponents: [
|
||||
{
|
||||
type: 'textEditor',
|
||||
item: {
|
||||
uri: `git://github.com/gorilla/mux?9e1f5955c0d22b55d9e20d6faa28589f83b2faca#mux.go`,
|
||||
languageId: 'go',
|
||||
text: '',
|
||||
},
|
||||
selections: [],
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
this.componentUpdates.next()
|
||||
}
|
||||
|
||||
private getLSPTextDocumentPositionParams(
|
||||
position: HoveredToken & RepoSpec & RevSpec & FileSpec & ResolvedRevSpec
|
||||
): RepoSpec & RevSpec & ResolvedRevSpec & FileSpec & PositionSpec & ModeSpec {
|
||||
return {
|
||||
repoName: position.repoName,
|
||||
filePath: position.filePath,
|
||||
commitID: position.commitID,
|
||||
rev: position.rev,
|
||||
mode: getModeFromPath(FILE_PATH),
|
||||
position,
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!this.state.blobOrError) {
|
||||
// Render placeholder for layout before content is fetched.
|
||||
return <div className="blob-page__placeholder">Loading...</div>
|
||||
}
|
||||
|
||||
const hoverOverlayProps = this.adjustHoverOverlayPosition(this.target)
|
||||
|
||||
return (
|
||||
<div className={this.props.className} ref={this.nextCodeIntellifyBlobElements}>
|
||||
<div className="code-header">
|
||||
<span className="code-header__title">github.com/gorilla/mux/mux.go</span>
|
||||
<span className="code-header__link">
|
||||
<a href="https://sourcegraph.com/github.com/gorilla/mux/-/blob/mux.go">View full file</a>
|
||||
</span>
|
||||
</div>
|
||||
{!isErrorLike(this.state.blobOrError) && (
|
||||
<code
|
||||
className={`blob__code blob__code--wrapped e2e-blob`}
|
||||
ref={this.nextCodeViewElement}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: trimHTMLString(
|
||||
this.state.blobOrError.highlight.html,
|
||||
this.props.startLine - 1,
|
||||
this.props.endLine + 1
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{this.state.hoverOverlayProps && (
|
||||
<HoverOverlay
|
||||
{...hoverOverlayProps}
|
||||
hoverRef={this.nextOverlayElement}
|
||||
extensionsController={this.props.extensionsController}
|
||||
platformContext={this.props.platformContext}
|
||||
location={this.props.location}
|
||||
onCloseButtonClick={this.nextCloseButtonClick}
|
||||
showCloseButton={false}
|
||||
className={this.props.tooltipClass}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/**
|
||||
* This function adjusts the position of the hoverOverlay so that it does not overflow on the right side
|
||||
* of the viewport. If a hoverOverlay will exceed the viewport, this function will adjust the position
|
||||
* so that it aligns the right side of the hover overlay with the right side of the target element.
|
||||
*
|
||||
*/
|
||||
private adjustHoverOverlayPosition(
|
||||
target: EventTarget | null
|
||||
): HoverState<HoverContext, HoverMerged, ActionItemProps>['hoverOverlayProps'] {
|
||||
const viewPortEdge = window.innerWidth
|
||||
if (!this.state.hoverOverlayProps) {
|
||||
return undefined
|
||||
}
|
||||
if (!target) {
|
||||
return this.state.hoverOverlayProps
|
||||
}
|
||||
const { overlayPosition, ...rest } = this.state.hoverOverlayProps
|
||||
|
||||
const targetBounds = (target as HTMLElement).getBoundingClientRect()
|
||||
let newOverlayPosition: { top: number; left: number } = overlayPosition!
|
||||
|
||||
if (overlayPosition && viewPortEdge < targetBounds.left + 512 && targetBounds.right - 512 >= 0) {
|
||||
const containerWidth = (document.querySelector(
|
||||
this.props.parentElement
|
||||
) as HTMLElement).parentElement!.getBoundingClientRect().width
|
||||
|
||||
const parentWidth = (document.querySelector(
|
||||
this.props.parentElement
|
||||
) as HTMLElement).getBoundingClientRect().width
|
||||
|
||||
// One side of the total horizontal margin.
|
||||
const halfMarginWidth = (viewPortEdge - containerWidth) / 2
|
||||
// The difference between the viewport width and parent width. We need to subtract this because
|
||||
// `left` is relative to the parent, whereas targetBounds.right is relative to the viewport.
|
||||
const relativeElementDifference = viewPortEdge - parentWidth
|
||||
|
||||
newOverlayPosition = {
|
||||
top: overlayPosition.top,
|
||||
// 512 is the width of a hoverOverlay.
|
||||
left: targetBounds.right - 512 - relativeElementDifference + halfMarginWidth,
|
||||
}
|
||||
}
|
||||
return { ...rest, overlayPosition: newOverlayPosition }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can only fetch blobs as an entire file. For demo purposes, we only want to show part of the file.
|
||||
* This function trims the HTML string of the file that will be code-intellfied on the homepage to only show
|
||||
* the lines that we specify. It makes some assumptions for this specific use case, such as the presence
|
||||
* of a single table and tbody element in the html, so be careful when changing.
|
||||
*/
|
||||
function trimHTMLString(html: string, startLine: number, endLine: number): string {
|
||||
const domParser = new DOMParser()
|
||||
const doc = domParser.parseFromString(html, 'text/html')
|
||||
const startToRemove = doc.querySelectorAll(`tr:nth-child(n + 0):nth-child(-n + ${startLine})`)
|
||||
const endToRemove = doc.querySelectorAll(`tr:nth-child(n + ${endLine})`)
|
||||
|
||||
const elementsToRemove = [...startToRemove, ...endToRemove]
|
||||
const tableEl = doc.querySelector('tbody')! // assume a single tbody element will exist in blob HTML
|
||||
|
||||
for (const el of elementsToRemove) {
|
||||
tableEl.removeChild(el)
|
||||
}
|
||||
|
||||
const xmlSerializer = new XMLSerializer()
|
||||
const tbl = doc.querySelector('table')! // assume a single table element will exist in blob HTML
|
||||
const trimmedHTMLString = xmlSerializer.serializeToString(tbl)
|
||||
|
||||
return trimmedHTMLString
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,76 +0,0 @@
|
||||
@import './Suggestion';
|
||||
|
||||
.query-input2-for-modal {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&__input {
|
||||
// Right side is flush with SearchButton.
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&__suggestions {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
max-height: 25rem;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-y: overlay;
|
||||
background-color: #000000;
|
||||
border: solid 1px #1b548a;
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
|
||||
.theme-light & {
|
||||
background-color: $color-light-bg-2;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
// Custom scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #2a3a51;
|
||||
}
|
||||
|
||||
.theme-light &::-webkit-scrollbar-thumb {
|
||||
background-color: $color-light-bg-3;
|
||||
}
|
||||
|
||||
* {
|
||||
display: inline-block;
|
||||
}
|
||||
li {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading-notifier {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 0.5rem;
|
||||
padding-left: 0.25rem;
|
||||
bottom: 0.375rem;
|
||||
background-color: $color-bg-4;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
.query-input2 {
|
||||
&__loading-notifier {
|
||||
background-color: $color-light-bg-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,425 +0,0 @@
|
||||
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
|
||||
import * as H from 'history'
|
||||
import * as React from 'react'
|
||||
import { fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
delay,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
publishReplay,
|
||||
refCount,
|
||||
repeat,
|
||||
startWith,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
toArray,
|
||||
} from 'rxjs/operators'
|
||||
import { Key } from 'ts-key-enum'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { scrollIntoView } from '../../util'
|
||||
import { fetchSuggestions } from '../backend'
|
||||
import { createSuggestion, Suggestion, SuggestionItem } from './Suggestion'
|
||||
|
||||
/**
|
||||
* The query input field is clobbered and updated to contain this subject's values, as
|
||||
* they are received. This is used to trigger an update; the source of truth is still the URL.
|
||||
*
|
||||
* This file is mostly the same as Queryinput.tsx but differs for the use on the homepage on line
|
||||
* 326-335. It also does not need onInputFocus function.
|
||||
*
|
||||
*/
|
||||
export const queryUpdates = new Subject<string>()
|
||||
|
||||
interface Props {
|
||||
location: H.Location
|
||||
history: H.History
|
||||
|
||||
/** The value of the query input */
|
||||
value: string
|
||||
|
||||
/** Called when the value changes */
|
||||
onChange: (newValue: string) => void
|
||||
|
||||
/**
|
||||
* A string that is appended to the query input's query before
|
||||
* fetching suggestions.
|
||||
*/
|
||||
prependQueryForSuggestions?: string
|
||||
|
||||
/** Whether the input should be autofocused (and the behavior thereof) */
|
||||
autoFocus?: true | 'cursor-at-end'
|
||||
|
||||
/** The input placeholder, if different from the default is desired. */
|
||||
placeholder?: string
|
||||
|
||||
/**
|
||||
* Whether this input should behave like the global query input: (1)
|
||||
* pressing the '/' key focuses it and (2) other components contribute a
|
||||
* query to it with their context (such as the repository area contributing
|
||||
* 'repo:foo@bar' for the current repository and revision).
|
||||
*
|
||||
* At most one query input per page should have this behavior.
|
||||
*/
|
||||
hasGlobalQueryBehavior?: boolean
|
||||
}
|
||||
|
||||
interface State {
|
||||
/** Whether the query input is focused */
|
||||
inputFocused: boolean
|
||||
|
||||
/** Whether suggestions are shown or not */
|
||||
hideSuggestions: boolean
|
||||
|
||||
/** The suggestions shown to the user */
|
||||
suggestions: Suggestion[]
|
||||
|
||||
/** Index of the currently selected suggestion (-1 if none selected) */
|
||||
selectedSuggestion: number
|
||||
|
||||
/** Whether suggestions are currently being fetched */
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export class QueryInputForModal extends React.Component<Props, State> {
|
||||
private static SUGGESTIONS_QUERY_MIN_LENGTH = 2
|
||||
|
||||
private componentUpdates = new Subject<Props>()
|
||||
|
||||
/** Subscriptions to unsubscribe from on component unmount */
|
||||
private subscriptions = new Subscription()
|
||||
|
||||
/** Emits on keydown events in the input field */
|
||||
private inputKeyDowns = new Subject<React.KeyboardEvent<HTMLInputElement>>()
|
||||
|
||||
/** Emits new input values */
|
||||
private inputValues = new Subject<string>()
|
||||
|
||||
/** Emits when the input field is clicked */
|
||||
private inputFocuses = new Subject<void>()
|
||||
|
||||
/** Emits when the suggestions are hidden */
|
||||
private suggestionsHidden = new Subject<void>()
|
||||
|
||||
/** Only used for selection and focus management */
|
||||
private inputElement?: HTMLInputElement
|
||||
|
||||
/** Only used for scroll state management */
|
||||
private suggestionListElement?: HTMLElement
|
||||
|
||||
/** Only used for scroll state management */
|
||||
private selectedSuggestionElement?: HTMLElement
|
||||
|
||||
/** Only used to keep track if the user has typed a single character into the input field so we can log an event once. */
|
||||
private hasLoggedFirstInput = false
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
hideSuggestions: false,
|
||||
inputFocused: false,
|
||||
loading: false,
|
||||
selectedSuggestion: -1,
|
||||
suggestions: [],
|
||||
}
|
||||
|
||||
this.subscriptions.add(
|
||||
// Trigger new suggestions every time the input field is typed into
|
||||
this.inputValues
|
||||
.pipe(
|
||||
tap(query => this.props.onChange(query)),
|
||||
distinctUntilChanged(),
|
||||
debounceTime(200),
|
||||
switchMap(query => {
|
||||
if (query.length < QueryInputForModal.SUGGESTIONS_QUERY_MIN_LENGTH) {
|
||||
return [{ suggestions: [], selectedSuggestion: -1, loading: false }]
|
||||
}
|
||||
const fullQuery = [this.props.prependQueryForSuggestions, this.props.value]
|
||||
.filter(s => !!s)
|
||||
.join(' ')
|
||||
const suggestionsFetch = fetchSuggestions(fullQuery).pipe(
|
||||
map(createSuggestion),
|
||||
toArray(),
|
||||
map((suggestions: Suggestion[]) => ({
|
||||
suggestions,
|
||||
selectedSuggestion: -1,
|
||||
hideSuggestions: false,
|
||||
loading: false,
|
||||
})),
|
||||
catchError((err: Error) => {
|
||||
console.error(err)
|
||||
this.setState({ loading: false })
|
||||
// HACK: if we catchError before 100ms, then the loader will display over us.
|
||||
// This is not a good fix.
|
||||
setTimeout(() => this.setState({ loading: false }), 120)
|
||||
return [{}]
|
||||
}),
|
||||
publishReplay(),
|
||||
refCount()
|
||||
)
|
||||
return merge(
|
||||
suggestionsFetch,
|
||||
// Show a loader if the fetch takes longer than 100ms
|
||||
of({ loading: true }).pipe(
|
||||
delay(100),
|
||||
takeUntil(suggestionsFetch)
|
||||
)
|
||||
)
|
||||
}),
|
||||
// Abort suggestion display on route change or suggestion hiding
|
||||
takeUntil(this.suggestionsHidden),
|
||||
// But resubscribe afterwards
|
||||
repeat()
|
||||
)
|
||||
.subscribe(
|
||||
state => {
|
||||
this.setState(state as State)
|
||||
},
|
||||
err => {
|
||||
this.setState({ loading: false })
|
||||
console.error(err)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (this.props.hasGlobalQueryBehavior) {
|
||||
// Quick-Open hotkeys
|
||||
this.subscriptions.add(
|
||||
fromEvent<KeyboardEvent>(window, 'keydown')
|
||||
.pipe(
|
||||
filter(
|
||||
event =>
|
||||
// Slash shortcut (if no input element is focused)
|
||||
(event.key === '/' &&
|
||||
!!document.activeElement &&
|
||||
!['INPUT', 'TEXTAREA'].includes(document.activeElement.nodeName)) ||
|
||||
// Cmd/Ctrl+P shortcut
|
||||
((event.metaKey || event.ctrlKey) && event.key === 'p') ||
|
||||
// Cmd/Ctrl+Shift+F shortcut
|
||||
((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'f')
|
||||
),
|
||||
switchMap(event => {
|
||||
event.preventDefault()
|
||||
// Use selection as query
|
||||
const selection = window.getSelection().toString()
|
||||
if (selection) {
|
||||
return new Observable<void>(observer =>
|
||||
this.setState(
|
||||
{
|
||||
// query: selection, TODO(sqs): add back this behavior
|
||||
suggestions: [],
|
||||
selectedSuggestion: -1,
|
||||
},
|
||||
() => {
|
||||
observer.next()
|
||||
observer.complete()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
return [undefined]
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
if (this.inputElement) {
|
||||
// Select all input
|
||||
this.inputElement.focus()
|
||||
this.inputElement.setSelectionRange(0, this.inputElement.value.length)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Allow other components to update the query (e.g., to be relevant to what the user is
|
||||
// currently viewing).
|
||||
this.subscriptions.add(
|
||||
queryUpdates.pipe(distinctUntilChanged()).subscribe(query => this.props.onChange(query))
|
||||
)
|
||||
|
||||
/** Whenever the URL query has a "focus" property, remove it and focus the query input. */
|
||||
this.subscriptions.add(
|
||||
this.componentUpdates.pipe(startWith(props)).subscribe(props => {
|
||||
if (this.inputElement) {
|
||||
const value = this.inputElement.value
|
||||
if (value !== props.value) {
|
||||
this.inputElement.value = props.value
|
||||
this.focusInputAndPositionCursorAtEnd()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
switch (this.props.autoFocus) {
|
||||
case 'cursor-at-end':
|
||||
this.focusInputAndPositionCursorAtEnd()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(newProps: Props): void {
|
||||
this.componentUpdates.next(newProps)
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State): void {
|
||||
// Check if selected suggestion is out of view
|
||||
scrollIntoView(this.suggestionListElement, this.selectedSuggestionElement)
|
||||
}
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
const showSuggestions =
|
||||
this.props.value.length >= QueryInputForModal.SUGGESTIONS_QUERY_MIN_LENGTH &&
|
||||
this.state.inputFocused &&
|
||||
!this.state.hideSuggestions &&
|
||||
this.state.suggestions.length !== 0
|
||||
|
||||
return (
|
||||
<div className="query-input2-for-modal">
|
||||
<input
|
||||
className="form-control query-input2-for-modal__input"
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.autoFocus === true}
|
||||
onChange={this.onInputChange}
|
||||
onKeyDown={this.onInputKeyDown}
|
||||
onFocus={this.changeCursor}
|
||||
onBlur={this.onInputBlur}
|
||||
spellCheck={false}
|
||||
autoCapitalize="off"
|
||||
placeholder={this.props.placeholder === undefined ? 'Search code...' : this.props.placeholder}
|
||||
ref={ref => (this.inputElement = ref!)}
|
||||
/>
|
||||
{this.state.loading && <LoadingSpinner className="icon-inline query-input2__loading-notifier" />}
|
||||
{showSuggestions && (
|
||||
<ul className="query-input2-for-modal__suggestions" ref={this.setSuggestionListElement}>
|
||||
{this.state.suggestions.map((suggestion, i) => {
|
||||
const isSelected = this.state.selectedSuggestion === i
|
||||
const onRef = (ref: HTMLLIElement | null) => {
|
||||
if (isSelected) {
|
||||
this.selectedSuggestionElement = ref || undefined
|
||||
}
|
||||
}
|
||||
return (
|
||||
<SuggestionItem
|
||||
key={i}
|
||||
suggestion={suggestion}
|
||||
isSelected={isSelected}
|
||||
// tslint:disable-next-line:jsx-no-lambda
|
||||
onClick={() => this.selectSuggestion(suggestion)}
|
||||
liRef={onRef}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle the cursor changing in the input field and
|
||||
// allow the user to type after the string changes.
|
||||
// Used in modal on homepage.
|
||||
private changeCursor: React.FocusEventHandler<HTMLInputElement> = e => {
|
||||
setTimeout(() => {
|
||||
this.inputFocuses.next()
|
||||
this.setState({ inputFocused: true })
|
||||
}, 100)
|
||||
}
|
||||
private setSuggestionListElement = (ref: HTMLElement | null): void => {
|
||||
this.suggestionListElement = ref || undefined
|
||||
}
|
||||
|
||||
private selectSuggestion = (suggestion: Suggestion): void => {
|
||||
// 🚨 PRIVACY: never provide any private data in { code_search: { suggestion: { type } } }.
|
||||
eventLogger.log('SearchSuggestionSelected', {
|
||||
code_search: {
|
||||
suggestion: {
|
||||
type: suggestion.type,
|
||||
url: suggestion.url,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
this.props.history.push(suggestion.url)
|
||||
|
||||
this.suggestionsHidden.next()
|
||||
this.setState({ hideSuggestions: true, selectedSuggestion: -1 })
|
||||
}
|
||||
|
||||
private focusInputAndPositionCursorAtEnd(): void {
|
||||
if (this.inputElement) {
|
||||
// Focus the input element and set cursor to the end
|
||||
this.inputElement.focus()
|
||||
this.inputElement.setSelectionRange(this.inputElement.value.length, this.inputElement.value.length)
|
||||
}
|
||||
}
|
||||
|
||||
private onInputChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||
if (!this.hasLoggedFirstInput) {
|
||||
eventLogger.log('SearchInitiated')
|
||||
this.hasLoggedFirstInput = true
|
||||
}
|
||||
this.inputValues.next(event.currentTarget.value)
|
||||
}
|
||||
|
||||
private onInputBlur: React.FocusEventHandler<HTMLInputElement> = event => {
|
||||
this.suggestionsHidden.next()
|
||||
this.setState({ inputFocused: false, loading: false, hideSuggestions: true })
|
||||
}
|
||||
|
||||
private onInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = event => {
|
||||
event.persist()
|
||||
this.inputKeyDowns.next(event)
|
||||
switch (event.key) {
|
||||
case Key.Escape: {
|
||||
this.suggestionsHidden.next()
|
||||
this.setState({ loading: false, hideSuggestions: true, selectedSuggestion: -1 })
|
||||
break
|
||||
}
|
||||
case Key.ArrowDown: {
|
||||
event.preventDefault()
|
||||
this.moveSelection(1)
|
||||
break
|
||||
}
|
||||
case Key.ArrowUp: {
|
||||
event.preventDefault()
|
||||
this.moveSelection(-1)
|
||||
break
|
||||
}
|
||||
case Key.Enter: {
|
||||
if (this.state.selectedSuggestion === -1) {
|
||||
// Submit form and hide suggestions
|
||||
this.suggestionsHidden.next()
|
||||
this.setState({ hideSuggestions: true })
|
||||
break
|
||||
}
|
||||
|
||||
// Select suggestion
|
||||
event.preventDefault()
|
||||
if (this.state.suggestions.length === 0) {
|
||||
break
|
||||
}
|
||||
this.selectSuggestion(this.state.suggestions[Math.max(this.state.selectedSuggestion, 0)])
|
||||
this.setState({ hideSuggestions: true })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private moveSelection(steps: number): void {
|
||||
this.setState({
|
||||
selectedSuggestion: Math.max(
|
||||
Math.min(this.state.selectedSuggestion + steps, this.state.suggestions.length - 1),
|
||||
-1
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
Components for areas in the app that expose a search input
|
||||
@ -10,7 +10,7 @@ class ServerAdminWrapper {
|
||||
private isAuthenicated = false
|
||||
|
||||
constructor() {
|
||||
if (!window.context.sourcegraphDotComMode) {
|
||||
if (window.context && !window.context.sourcegraphDotComMode) {
|
||||
authenticatedUser.subscribe(user => {
|
||||
if (user) {
|
||||
this.isAuthenicated = true
|
||||
|
||||
@ -11,7 +11,7 @@ class TelligentWrapper {
|
||||
|
||||
constructor() {
|
||||
// Never log anything in self-hosted Sourcegraph instances.
|
||||
if (!window.context.sourcegraphDotComMode) {
|
||||
if (!window.context || !window.context.sourcegraphDotComMode) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -2046,6 +2046,11 @@
|
||||
"@types/events" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/classnames@^2.2.7":
|
||||
version "2.2.7"
|
||||
resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.7.tgz#fb68cc9be8487e6ea5b13700e759bfbab7e0fefd"
|
||||
integrity sha512-rzOhiQ55WzAiFgXRtitP/ZUT8iVNyllEpylJ5zHzR4vArUvMB39GTk+Zon/uAM0JxEFAWnwsxC2gH8s+tZ3Myg==
|
||||
|
||||
"@types/cli-color@^0.3.29":
|
||||
version "0.3.29"
|
||||
resolved "https://registry.npmjs.org/@types/cli-color/-/cli-color-0.3.29.tgz#c83a71fe02c8c7e1ccec048dd6a2458d1f6c96ea"
|
||||
@ -4663,7 +4668,7 @@ class-utils@^0.3.5:
|
||||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classnames@^2.2.3, classnames@^2.2.5:
|
||||
classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6:
|
||||
version "2.2.6"
|
||||
resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user