[now-static-build][frameworks][examples] Fixes examples and adjust frameworks (#3584)

* [examples] Fix ionic-react example

* [examples] Fix vue example

* [examples] Fix mithril example

* [examples] Fix riot example

* Fix readmes

* [now-static-build] Add Zola

* Add tests

* [now-build-utils][frameworks] Adjust detect framework

* Move zola back

* Undo Hugo detection changes

* [examples] Fix Vue logo path

* [now-static-build] Use package.json script if defined instead of framework command

* [now-static-build] Add buildCommand everywhere

* Remove devCommand from frameworks.ts

* Fix type

* Change output directory

* [now-static-build] Remove minNodeRange

* Remove devCommand
This commit is contained in:
Andy
2020-01-16 00:12:55 +01:00
committed by GitHub
parent 0a63bd47e8
commit 96dbc6d348
145 changed files with 2051 additions and 3605 deletions

View File

@@ -1,4 +1,4 @@
![Angular Logo](../packages/frameworks/logos/angular.svg) ![Angular Logo](../../packages/frameworks/logos/angular.svg)
# Angular Example # Angular Example

View File

@@ -1,4 +1,4 @@
![React Logo](../packages/frameworks/logos/react.svg) ![React Logo](../../packages/frameworks/logos/react.svg)
# React Example # React Example

View File

@@ -1,4 +1,4 @@
![Docusaurus Logo](../packages/frameworks/logos/docusaurus.svg) ![Docusaurus Logo](../../packages/frameworks/logos/docusaurus.svg)
# Docusaurus Example # Docusaurus Example

View File

@@ -1,4 +1,4 @@
![Eleventy Logo](../packages/frameworks/logos/eleventy.svg) ![Eleventy Logo](../../packages/frameworks/logos/eleventy.svg)
# Eleventy Example # Eleventy Example

View File

@@ -1,4 +1,4 @@
![Ember Logo](../packages/frameworks/logos/ember.svg) ![Ember Logo](../../packages/frameworks/logos/ember.svg)
# Ember Example # Ember Example

View File

@@ -1,4 +1,4 @@
![Gatsby Logo](../packages/frameworks/logos/gatsby.svg) ![Gatsby Logo](../../packages/frameworks/logos/gatsby.svg)
# Gatsby Example # Gatsby Example

View File

@@ -1,4 +1,4 @@
![Gridsome Logo](../packages/frameworks/logos/gridsome.svg) ![Gridsome Logo](../../packages/frameworks/logos/gridsome.svg)
# Gridsome Example # Gridsome Example

View File

@@ -1,4 +1,4 @@
![Hexo Logo](../packages/frameworks/logos/hexo.svg) ![Hexo Logo](../../packages/frameworks/logos/hexo.svg)
# Hexo Example # Hexo Example

View File

@@ -1,15 +0,0 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@@ -1,67 +1,9 @@
# Logs # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.firebase
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules /node_modules
/.pnp
.pnp.js
# testing # testing
/coverage /coverage
@@ -75,9 +17,8 @@ typings/
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.vscode
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.stencil/

View File

@@ -1,8 +0,0 @@
{
"appId": "io.ionic.starter",
"appName": "ionic-react-conference-app",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "build",
"cordova": {}
}

View File

@@ -1,7 +1,5 @@
{ {
"name": "ionic-react-conference-app", "name": "ionic-react",
"integrations": { "integrations": {},
"capacitor": {}
},
"type": "react" "type": "react"
} }

View File

@@ -1,26 +1,26 @@
{ {
"name": "ionic-react-conference-app", "name": "ionic-react",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@capacitor/core": "1.3.0", "@ionic/react": "^4.11.0",
"@ionic/react": "^4.11.4", "@ionic/react-router": "^4.11.0",
"@ionic/react-router": "^4.11.4", "@testing-library/jest-dom": "^4.2.4",
"@types/jest": "24.0.18", "@testing-library/react": "^9.4.0",
"@types/node": "12.7.5", "@testing-library/user-event": "^8.0.3",
"@types/react": "^16.9.2", "@types/jest": "^24.0.25",
"@types/react-dom": "^16.9.0", "@types/node": "^12.12.24",
"@types/react-router": "^5.0.3", "@types/react": "^16.9.17",
"@types/react-router-dom": "^4.3.1", "@types/react-dom": "^16.9.4",
"date-fns": "^2.6.0", "@types/react-router": "^5.1.4",
"@types/react-router-dom": "^5.1.3",
"ionicons": "^4.6.3", "ionicons": "^4.6.3",
"node-sass": "^4.13.0", "react": "^16.12.0",
"react": "^16.9.0", "react-dom": "^16.12.0",
"react-dom": "^16.9.0", "react-router": "^5.1.2",
"react-router": "^5.0.1", "react-router-dom": "^5.1.2",
"react-router-dom": "^5.0.1", "react-scripts": "3.3.0",
"react-scripts": "3.2.0", "typescript": "3.7.4"
"reselect": "^4.0.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
@@ -28,17 +28,20 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"browserslist": [ "eslintConfig": {
">0.2%", "extends": "react-app"
"not dead", },
"not ie <= 11", "browserslist": {
"not op_mini all" "production": [
], ">0.2%",
"description": "An Ionic project", "not dead",
"devDependencies": { "not op_mini all"
"@capacitor/cli": "1.3.0", ],
"@testing-library/react": "^9.3.1", "development": [
"@types/googlemaps": "^3.38.0", "last 1 chrome version",
"typescript": "3.6.3" "last 1 firefox version",
} "last 1 safari version"
]
},
"description": "An Ionic project"
} }

View File

@@ -1,12 +1,8 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import { render } from '@testing-library/react';
import App from './App'; import App from './App';
import { render, fireEvent, waitForElement } from '@testing-library/react'
it('renders without crashing', () => { test('renders without crashing', () => {
// const div = document.createElement('div'); const { baseElement } = render(<App />);
// ReactDOM.render(<App />, div); expect(baseElement).toBeDefined();
// ReactDOM.unmountComponentAtNode(div);
const { asFragment, container } = render(<App />);
expect(asFragment()).toMatchSnapshot();
}); });

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from 'react'; import React from 'react';
import { Redirect, Route } from 'react-router-dom'; import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, IonSplitPane } from '@ionic/react'; import { IonApp, IonRouterOutlet } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router'; import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';
import Menu from './components/Menu';
/* Core CSS required for Ionic components to work properly */ /* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css'; import '@ionic/react/css/core.css';
@@ -23,85 +22,16 @@ import '@ionic/react/css/display.css';
/* Theme variables */ /* Theme variables */
import './theme/variables.css'; import './theme/variables.css';
import MainTabs from './pages/MainTabs';
import { connect } from './data/connect';
import { AppContextProvider } from './data/AppContext';
import { loadConfData } from './data/sessions/sessions.actions';
import { setIsLoggedIn, setUsername, loadUserData } from './data/user/user.actions';
import Account from './pages/Account';
import Login from './pages/Login';
import Signup from './pages/Signup';
import Support from './pages/Support';
import Tutorial from './pages/Tutorial';
import HomeOrTutorial from './components/HomeOrTutorial';
import { Session } from "./models/Session";
const App: React.FC = () => { const App: React.FC = () => (
return ( <IonApp>
<AppContextProvider> <IonReactRouter>
<IonicAppConnected /> <IonRouterOutlet>
</AppContextProvider> <Route path="/home" component={Home} exact={true} />
); <Route exact path="/" render={() => <Redirect to="/home" />} />
}; </IonRouterOutlet>
</IonReactRouter>
interface StateProps { </IonApp>
darkMode: boolean, );
sessions: Session[],
}
interface DispatchProps {
loadConfData: typeof loadConfData;
loadUserData: typeof loadUserData;
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface IonicAppProps extends StateProps, DispatchProps { }
const IonicApp: React.FC<IonicAppProps> = ({ darkMode, sessions, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => {
useEffect(() => {
loadUserData();
loadConfData();
// eslint-disable-next-line
}, []);
return (
sessions.length === 0 ? (
<div></div>
) : (
<IonApp className={`${darkMode ? 'dark-theme' : ''}`}>
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/tabs" component={MainTabs} />
<Route path="/account" component={Account} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/support" component={Support} />
<Route path="/tutorial" component={Tutorial} />
<Route path="/logout" render={() => {
setIsLoggedIn(false);
setUsername(undefined);
return <Redirect to="/tabs" />
}} />
<Route path="/" component={HomeOrTutorial} exact />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
)
)
}
export default App; export default App;
const IonicAppConnected = connect<{}, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
darkMode: state.user.darkMode,
sessions: state.data.sessions
}),
mapDispatchToProps: { loadConfData, loadUserData, setIsLoggedIn, setUsername },
component: IonicApp
});

View File

@@ -1,280 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders without crashing 1`] = `
<DocumentFragment>
<ion-app>
<ion-split-pane
content-id="main"
>
<ion-menu
content-id="main"
>
<ion-header>
<ion-toolbar>
<ion-title>
Menu
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content
class="outer-content"
>
<ion-list>
<ion-list-header>
Navigate
</ion-list-header>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Schedule
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Speakers
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Map
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
About
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list>
<ion-list-header>
Account
</ion-list-header>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Account
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Support
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Logout
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list>
<ion-list-header>
Tutorial
</ion-list-header>
<ion-item>
<ion-icon
slot="start"
/>
Show Tutorial
</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet
id="main"
>
<div
style="display: flex; position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px; flex-direction: column; width: 100%; height: 100%; contain: layout size style;"
>
<div
class="tabs-inner"
style="position: relative; flex: 1; contain: layout size style;"
>
<ion-router-outlet>
<div
class="ion-page ion-page-invisible"
>
<ion-header>
<ion-toolbar
color="primary"
>
<ion-buttons
slot="start"
>
<ion-menu-button />
</ion-buttons>
<ion-segment>
<ion-segment-button
value="all"
>
All
</ion-segment-button>
<ion-segment-button
value="favorites"
>
Favorites
</ion-segment-button>
</ion-segment>
<ion-buttons
slot="end"
>
<ion-button>
<ion-icon
slot="icon-only"
/>
</ion-button>
</ion-buttons>
</ion-toolbar>
<ion-toolbar
color="primary"
>
<ion-searchbar
placeholder="Search"
/>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher
slot="fixed"
>
<ion-refresher-content />
</ion-refresher>
<ion-list>
<ion-list-header>
No Sessions Found
</ion-list-header>
</ion-list>
<ion-list
style="display: none;"
/>
</ion-content>
<ion-fab
horizontal="end"
slot="fixed"
vertical="bottom"
>
<ion-fab-button>
<ion-icon />
</ion-fab-button>
<ion-fab-list
side="top"
>
<ion-fab-button
color="vimeo"
>
<ion-icon />
</ion-fab-button>
<ion-fab-button
color="google"
>
<ion-icon />
</ion-fab-button>
<ion-fab-button
color="twitter"
>
<ion-icon />
</ion-fab-button>
<ion-fab-button
color="facebook"
>
<ion-icon />
</ion-fab-button>
</ion-fab-list>
</ion-fab>
</div>
</ion-router-outlet>
</div>
<ion-tab-bar
current-path="/tabs/schedule"
selected-tab="schedule"
slot="bottom"
>
<ion-tab-button
href="/tabs/schedule"
tab="schedule"
>
<ion-icon />
<ion-label>
Schedule
</ion-label>
</ion-tab-button>
<ion-tab-button
href="/tabs/speakers"
tab="speakers"
>
<ion-icon />
<ion-label>
Speakers
</ion-label>
</ion-tab-button>
<ion-tab-button
href="/tabs/map"
tab="map"
>
<ion-icon />
<ion-label>
Map
</ion-label>
</ion-tab-button>
<ion-tab-button
href="/tabs/about"
tab="about"
>
<ion-icon />
<ion-label>
About
</ion-label>
</ion-tab-button>
</ion-tab-bar>
</div>
</ion-router-outlet>
</ion-split-pane>
</ion-app>
</DocumentFragment>
`;

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { IonList, IonItem, IonLabel } from '@ionic/react';
interface AboutPopoverProps {
dismiss: () => void;
};
const AboutPopover: React.FC<AboutPopoverProps> = ({dismiss}) => {
const close = (url: string) => {
window.open(url, '_blank');
dismiss();
};
return (
<IonList>
<IonItem button onClick={() => close('https://ionicframework.com/getting-started')}>
<IonLabel>Learn Ionic</IonLabel>
</IonItem>
<IonItem button onClick={() => close('https://ionicframework.com/docs/react')}>
<IonLabel>Documentation</IonLabel>
</IonItem>
<IonItem button onClick={() => close('https://showcase.ionicframework.com')}>
<IonLabel>Showcase</IonLabel>
</IonItem>
<IonItem button onClick={() => close('https://github.com/ionic-team/ionic')}>
<IonLabel>GitHub Repo</IonLabel>
</IonItem>
<IonItem button onClick={dismiss}>
<IonLabel>Support</IonLabel>
</IonItem>
</IonList >
)
}
export default AboutPopover;

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { connect } from '../data/connect';
import { Redirect } from 'react-router';
interface StateProps {
hasSeenTutorial: boolean;
}
const HomeOrTutorial: React.FC<StateProps> = ({ hasSeenTutorial }) => {
return hasSeenTutorial ? <Redirect to="/tabs/schedule" /> : <Redirect to="/tutorial" />
};
export default connect<{}, StateProps, {}>({
mapStateToProps: (state) => ({
hasSeenTutorial: state.user.hasSeenTutorial
}),
component: HomeOrTutorial
});

View File

@@ -1,56 +0,0 @@
import React, { useRef, useEffect } from 'react';
import { Location } from '../models/Location';
interface MapProps {
locations: Location[]
mapCenter: Location
}
const Map: React.FC<MapProps> = ({ mapCenter, locations }) => {
const mapEle = useRef<HTMLDivElement>(null);
const map = useRef<google.maps.Map>();
useEffect(() => {
map.current = new google.maps.Map(mapEle.current, {
center: {
lat: mapCenter.lat,
lng: mapCenter.lng
},
zoom: 16
});
addMarkers();
google.maps.event.addListenerOnce(map.current, 'idle', () => {
if (mapEle.current) {
mapEle.current.classList.add('show-map');
}
});
function addMarkers() {
locations.forEach((markerData) => {
let infoWindow = new google.maps.InfoWindow({
content: `<h5>${markerData.name}</h5>`
});
let marker = new google.maps.Marker({
position: new google.maps.LatLng(markerData.lat, markerData.lng),
map: map.current!,
title: markerData.name
});
marker.addListener('click', () => {
infoWindow.open(map.current!, marker);
});
});
}
}, [mapCenter, locations]);
return (
<div ref={mapEle} className="map-canvas"></div>
);
}
export default Map;

View File

@@ -1,119 +0,0 @@
import {
IonContent,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenu,
IonMenuToggle,
IonTitle,
IonToolbar,
IonToggle
} from '@ionic/react';
import { calendar, contacts, hammer, help, informationCircle, logIn, logOut, map, person, personAdd } from 'ionicons/icons';
import React, { useState } from 'react';
import { connect } from '../data/connect';
import { RouteComponentProps, withRouter } from 'react-router';
import { setDarkMode } from '../data/user/user.actions';
const routes = {
appPages: [
{ title: 'Schedule', path: '/tabs/schedule', icon: calendar },
{ title: 'Speakers', path: '/tabs/speakers', icon: contacts },
{ title: 'Map', path: '/tabs/map', icon: map },
{ title: 'About', path: '/tabs/about', icon: informationCircle }
],
loggedInPages: [
{ title: 'Account', path: '/account', icon: person },
{ title: 'Support', path: '/support', icon: help },
{ title: 'Logout', path: '/logout', icon: logOut }
],
loggedOutPages: [
{ title: 'Login', path: '/login', icon: logIn },
{ title: 'Support', path: '/support', icon: help },
{ title: 'Signup', path: '/signup', icon: personAdd }
]
};
interface Pages {
title: string,
path: string,
icon: { ios: string, md: string },
routerDirection?: string
}
interface StateProps {
darkMode: boolean;
isAuthenticated: boolean;
}
interface DispatchProps {
setDarkMode: typeof setDarkMode
}
interface MenuProps extends RouteComponentProps, StateProps, DispatchProps { }
const Menu: React.FC<MenuProps> = ({ darkMode, history, isAuthenticated, setDarkMode }) => {
const [disableMenu, setDisableMenu] = useState(false);
function renderlistItems(list: Pages[]) {
return list
.filter(route => !!route.path)
.map(p => (
<IonMenuToggle key={p.title} auto-hide="false">
<IonItem button routerLink={p.path} routerDirection="none">
<IonIcon slot="start" icon={p.icon} />
<IonLabel>{p.title}</IonLabel>
</IonItem>
</IonMenuToggle>
));
}
return (
<IonMenu type="overlay" disabled={disableMenu} contentId="main">
<IonHeader>
<IonToolbar>
<IonTitle>Menu</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="outer-content">
<IonList>
<IonListHeader>Navigate</IonListHeader>
{renderlistItems(routes.appPages)}
</IonList>
<IonList>
<IonListHeader>Account</IonListHeader>
{isAuthenticated ? renderlistItems(routes.loggedInPages) : renderlistItems(routes.loggedOutPages)}
</IonList>
<IonList>
<IonListHeader>Tutorial</IonListHeader>
<IonItem onClick={() => {
setDisableMenu(true);
history.push('/tutorial');
}}>
<IonIcon slot="start" icon={hammer} />
Show Tutorial
</IonItem>
</IonList>
<IonList>
<IonItem>
<IonLabel>Dark Theme</IonLabel>
<IonToggle checked={darkMode} onClick={() => setDarkMode(!darkMode)} />
</IonItem>
</IonList>
</IonContent>
</IonMenu>
);
};
export default connect<{}, StateProps, {}>({
mapStateToProps: (state) => ({
darkMode: state.user.darkMode,
isAuthenticated: state.user.isLoggedin
}),
mapDispatchToProps: ({
setDarkMode
}),
component: withRouter(Menu)
})

View File

@@ -1,92 +0,0 @@
import { IonItemDivider, IonItemGroup, IonLabel, IonList, IonListHeader, IonAlert, AlertButton } from '@ionic/react';
import React, { useState, useCallback } from 'react';
import { Session } from '../models/Session';
import SessionListItem from './SessionListItem';
import { SessionGroup } from '../models/SessionGroup';
import { Time } from '../components/Time';
import { connect } from '../data/connect';
import { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
interface OwnProps {
sessionGroups: SessionGroup[]
listType: 'all' | 'favorites'
hide: boolean;
}
interface StateProps {
favoriteSessions: number[];
}
interface DispatchProps {
addFavorite: typeof addFavorite;
removeFavorite: typeof removeFavorite;
}
interface SessionListProps extends OwnProps, StateProps, DispatchProps { };
const SessionList: React.FC<SessionListProps> = ({ addFavorite, removeFavorite, favoriteSessions, hide, sessionGroups, listType }) => {
const [showAlert, setShowAlert] = useState(false);
const [alertHeader, setAlertHeader] = useState('');
const [alertButtons, setAlertButtons] = useState<(AlertButton | string)[]>([]);
const handleShowAlert = useCallback((header: string, buttons: AlertButton[]) => {
setAlertHeader(header);
setAlertButtons(buttons);
setShowAlert(true);
}, []);
if (sessionGroups.length === 0 && !hide) {
return (
<IonList>
<IonListHeader>
No Sessions Found
</IonListHeader>
</IonList>
);
}
return (
<>
<IonList style={hide ? { display: 'none' } : {}}>
{sessionGroups.map((group, index: number) => (
<IonItemGroup key={`group-${index}`}>
<IonItemDivider sticky>
<IonLabel>
<Time date={group.startTime} />
</IonLabel>
</IonItemDivider>
{group.sessions.map((session: Session, sessionIndex: number) => (
<SessionListItem
onShowAlert={handleShowAlert}
isFavorite={favoriteSessions.indexOf(session.id) > -1}
onAddFavorite={addFavorite}
onRemoveFavorite={removeFavorite}
key={`group-${index}-${sessionIndex}`}
session={session}
listType={listType}
/>
))}
</IonItemGroup>
))}
</IonList>
<IonAlert
isOpen={showAlert}
header={alertHeader}
buttons={alertButtons}
onDidDismiss={() => setShowAlert(false)}
></IonAlert>
</>
);
};
export default connect({
mapStateToProps: (state) => ({
favoriteSessions: state.data.favorites
}),
mapDispatchToProps: ({
addFavorite,
removeFavorite
}),
component: SessionList
});

View File

@@ -1,4 +0,0 @@
.filter-icon {
margin: 7px 16px 7px 0;
}

View File

@@ -1,108 +0,0 @@
import React from 'react';
import { IonHeader, IonToolbar, IonButtons, IonButton, IonTitle, IonContent, IonList, IonListHeader, IonItem, IonLabel, IonToggle, IonFooter, IonIcon } from '@ionic/react';
import { logoAngular, call, document, logoIonic, hammer, restaurant, cog, colorPalette, construct, compass } from 'ionicons/icons';
import './SessionListFilter.css'
import { connect } from '../data/connect';
import { updateFilteredTracks } from '../data/sessions/sessions.actions';
interface OwnProps {
onDismissModal: () => void;
}
interface StateProps {
allTracks: string[],
filteredTracks: string[]
}
interface DispatchProps {
updateFilteredTracks: typeof updateFilteredTracks;
}
type SessionListFilterProps = OwnProps & StateProps & DispatchProps;
const SessionListFilter: React.FC<SessionListFilterProps> = ({ allTracks, filteredTracks, onDismissModal, updateFilteredTracks }) => {
const toggleTrackFilter = (track: string) => {
if (filteredTracks.indexOf(track) > -1) {
updateFilteredTracks(filteredTracks.filter(x => x !== track));
} else {
updateFilteredTracks([...filteredTracks, track]);
}
};
const handleDeselectAll = () => {
updateFilteredTracks([]);
};
const handleSelectAll = () => {
updateFilteredTracks([...allTracks]);
};
const iconMap: { [key: string]: any } = {
'Angular': logoAngular,
'Documentation': document,
'Food': restaurant,
'Ionic': logoIonic,
'Tooling': hammer,
'Design': colorPalette,
'Services': cog,
'Workshop': construct,
'Navigation': compass,
'Communication': call
}
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
Filter Sessions
</IonTitle>
<IonButtons slot="end">
<IonButton onClick={onDismissModal} strong>Done</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="outer-content">
<IonList>
<IonListHeader>Tracks</IonListHeader>
{allTracks.map((track, index) => (
<IonItem key={track}>
<IonIcon className="filter-icon" icon={iconMap[track]} color="medium" />
<IonLabel>{track}</IonLabel>
<IonToggle
onClick={() => toggleTrackFilter(track)}
checked={filteredTracks.indexOf(track) !== -1}
color="success"
value={track}
></IonToggle>
</IonItem>
))}
</IonList>
</IonContent>
<IonFooter>
<IonToolbar>
<IonButtons slot="start">
<IonButton onClick={handleDeselectAll}>Deselect All</IonButton>
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={handleSelectAll}>Select All</IonButton>
</IonButtons>
</IonToolbar>
</IonFooter>
</>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
allTracks: state.data.allTracks,
filteredTracks: state.data.filteredTracks
}),
mapDispatchToProps: {
updateFilteredTracks
},
component: SessionListFilter
})

View File

@@ -1,83 +0,0 @@
import React, { useRef, useState } from 'react';
import { IonItemSliding, IonAlert, IonItem, IonLabel, IonItemOptions, IonItemOption, AlertButton } from '@ionic/react';
import { Time } from './Time';
import { Session } from '../models/Session';
interface SessionListItemProps {
session: Session;
listType: "all" | "favorites";
onAddFavorite: (id: number) => void;
onRemoveFavorite: (id: number) => void;
onShowAlert: (header: string, buttons: AlertButton[]) => void;
isFavorite: boolean;
}
const SessionListItem: React.FC<SessionListItemProps> = ({ isFavorite, onAddFavorite, onRemoveFavorite, onShowAlert, session, listType }) => {
const ionItemSlidingRef = useRef<HTMLIonItemSlidingElement>(null)
const dismissAlert = () => {
ionItemSlidingRef.current && ionItemSlidingRef.current.close();
}
const removeFavoriteSession = () => {
onAddFavorite(session.id);
onShowAlert('Favorite already added', [
{
text: 'Cancel',
handler: dismissAlert
},
{
text: 'Remove',
handler: () => {
onRemoveFavorite(session.id);
dismissAlert();
}
}
]);
}
const addFavoriteSession = () => {
if (isFavorite) {
// woops, they already favorited it! What shall we do!?
// prompt them to remove it
removeFavoriteSession();
} else {
// remember this session as a user favorite
onAddFavorite(session.id);
onShowAlert('Favorite Added', [
{
text: 'OK',
handler: dismissAlert
}
]);
}
};
return (
<IonItemSliding ref={ionItemSlidingRef} class={'track-' + session.tracks[0].toLowerCase()}>
<IonItem routerLink={`/tabs/schedule/${session.id}`}>
<IonLabel>
<h3>{session.name}</h3>
<p>
<Time date={session.dateTimeStart} /> &mdash;&nbsp;
<Time date={session.dateTimeEnd} /> &mdash;&nbsp;
{session.location}
</p>
</IonLabel>
</IonItem>
<IonItemOptions>
{listType === "favorites" ?
<IonItemOption color="danger" onClick={() => removeFavoriteSession()}>
Remove
</IonItemOption>
:
<IonItemOption color="favorite" onClick={addFavoriteSession}>
Favorite
</IonItemOption>
}
</IonItemOptions>
</IonItemSliding>
);
};
export default React.memo(SessionListItem);

View File

@@ -1,46 +0,0 @@
import { IonLoading, IonFab, IonFabButton, IonIcon, IonFabList } from "@ionic/react"
import { share, logoVimeo, logoGoogleplus, logoTwitter, logoFacebook } from "ionicons/icons"
import React, { useState } from "react"
const ShareSocialFab: React.FC = () => {
const [loadingMessage, setLoadingMessage] = useState('')
const [showLoading, setShowLoading] = useState(false);
const openSocial = (network: string) => {
setLoadingMessage(`Posting to ${network}`);
setShowLoading(true);
};
return(
<>
<IonLoading
isOpen={showLoading}
message={loadingMessage}
duration={2000}
spinner="crescent"
onDidDismiss={() => setShowLoading(false)}
/>
<IonFab slot="fixed" vertical="bottom" horizontal="end">
<IonFabButton>
<IonIcon icon={share} />
</IonFabButton>
<IonFabList side="top">
<IonFabButton color="vimeo" onClick={() => openSocial('Vimeo')}>
<IonIcon icon={logoVimeo} />
</IonFabButton>
<IonFabButton color="google" onClick={() => openSocial('Google+')}>
<IonIcon icon={logoGoogleplus} />
</IonFabButton>
<IonFabButton color="twitter" onClick={() => openSocial('Twitter')}>
<IonIcon icon={logoTwitter} />
</IonFabButton>
<IonFabButton color="facebook" onClick={() => openSocial('Facebook')}>
<IonIcon icon={logoFacebook} />
</IonFabButton>
</IonFabList>
</IonFab>
</>
)
};
export default ShareSocialFab;

View File

@@ -1,125 +0,0 @@
import React, { useState } from 'react';
import { Session } from '../models/Session';
import { Speaker } from '../models/Speaker';
import { IonCard, IonCardHeader, IonItem, IonAvatar, IonCardContent, IonList, IonRow, IonCol, IonButton, IonIcon, IonActionSheet } from '@ionic/react';
import { logoTwitter, shareAlt, chatboxes } from 'ionicons/icons';
import { ActionSheetButton } from '@ionic/core';
interface SpeakerItemProps {
speaker: Speaker;
sessions: Session[];
}
const SpeakerItem: React.FC<SpeakerItemProps> = ({ speaker, sessions }) => {
const [showActionSheet, setShowActionSheet] = useState(false);
const [actionSheetButtons, setActionSheetButtons] = useState<ActionSheetButton[]>([]);
const [actionSheetHeader, setActionSheetHeader] = useState('');
function openSpeakerShare(speaker: Speaker) {
setActionSheetButtons([
{
text: 'Copy Link',
handler: () => {
console.log('Copy Link clicked');
}
},
{
text: 'Share via ...',
handler: () => {
console.log('Share via clicked');
}
},
{
text: 'Cancel',
role: 'cancel',
handler: () => {
console.log('Cancel clicked');
}
}
]);
setActionSheetHeader(`Share ${speaker.name}`);
setShowActionSheet(true);
}
function openContact(speaker: Speaker) {
setActionSheetButtons([
{
text: `Email ( ${speaker.email} )`,
handler: () => {
window.open('mailto:' + speaker.email);
}
},
{
text: `Call ( ${speaker.phone} )`,
handler: () => {
window.open('tel:' + speaker.phone);
}
}
]);
setActionSheetHeader(`Share ${speaker.name}`);
setShowActionSheet(true);
}
return (
<>
<IonCard className="speaker-card">
<IonCardHeader>
<IonItem button detail={false} routerLink={`/tabs/speakers/${speaker.id}`} lines="none">
<IonAvatar slot="start">
<img src={process.env.PUBLIC_URL + speaker.profilePic} alt="Speaker profile pic" />
</IonAvatar>
{speaker.name}
</IonItem>
</IonCardHeader>
<IonCardContent class="outer-content">
<IonList>
{sessions.map(session => (
<IonItem routerLink={`/tabs/speakers/sessions/${session.id}`} key={session.name}>
<h3>{session.name}</h3>
</IonItem>
))}
<IonItem button routerLink={`/tabs/speakers/${speaker.id}`}>
<h3>About {speaker.name}</h3>
</IonItem>
</IonList>
</IonCardContent>
<IonRow justify-content-center>
<IonCol text-left size="4">
<IonButton
fill="clear"
size="small"
color="primary"
href={`https://www.twitter.com/${speaker.twitter}`}
target="_blank"
>
<IonIcon slot="start" icon={logoTwitter} />
Tweet
</IonButton>
</IonCol>
<IonCol text-left size="4">
<IonButton fill="clear" size="small" color="primary" onClick={() => openSpeakerShare(speaker)}>
<IonIcon slot="start" icon={shareAlt} />
Share
</IonButton>
</IonCol>
<IonCol text-left size="4">
<IonButton fill="clear" size="small" color="primary" onClick={() => openContact(speaker)}>
<IonIcon slot="start" icon={chatboxes} />
Contact
</IonButton>
</IonCol>
</IonRow>
</IonCard>
<IonActionSheet
isOpen={showActionSheet}
header={actionSheetHeader}
onDidDismiss={() => setShowActionSheet(false)}
buttons={actionSheetButtons}
/>
</>
);
};
export default SpeakerItem;

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { format, parseISO as parseDate } from 'date-fns';
export const Time: React.FC<{ date: string }> = ({ date }) => (
<>
{format(parseDate(date), "h:mm aaaaa")}m
</>
)

View File

@@ -1,26 +0,0 @@
import React, { createContext, useReducer } from 'react';
import { initialState, AppState, reducers } from './state'
export interface AppContextState {
state: AppState;
dispatch: React.Dispatch<any>;
}
export const AppContext = createContext<AppContextState>({
state: initialState,
dispatch: () => undefined
});
export const AppContextProvider: React.FC = (props => {
const [store, dispatch] = useReducer(reducers, initialState);
return (
<AppContext.Provider value={{
state: store,
dispatch
}}>
{props.children}
</AppContext.Provider>
)
});

View File

@@ -1,14 +0,0 @@
export function combineReducers<R extends any>(reducers: R) {
type keys = keyof typeof reducers;
type returnType = { [K in keys]: ReturnType<typeof reducers[K]> };
const combinedReducer = (state: any, action: any) => {
const newState: returnType = {} as any;
const keys = Object.keys(reducers);
keys.forEach(key => {
const result = reducers[key](state[key], action);
newState[key as keys] = result || state[key];
});
return newState;
};
return combinedReducer;
}

View File

@@ -1,49 +0,0 @@
import React, { useContext, useEffect, useState, useMemo } from 'react';
import { AppContext } from './AppContext';
import { DispatchObject } from '../util/types';
import { AppState } from './state';
interface ConnectParams<TOwnProps, TStateProps, TDispatchProps> {
mapStateToProps?: (state: AppState, props: TOwnProps) => TStateProps,
mapDispatchToProps?: TDispatchProps,
component: React.ComponentType<any>
};
export function connect<TOwnProps = any, TStateProps = any, TDispatchProps = any>({ mapStateToProps = () => ({} as TStateProps), mapDispatchToProps = {} as TDispatchProps, component }: ConnectParams<TOwnProps, TStateProps, TDispatchProps>): React.FunctionComponent<TOwnProps> {
const Connect = (ownProps: TOwnProps) => {
const context = useContext(AppContext);
const dispatchFuncs = useMemo(() => {
const dispatchFuncs: { [key: string]: any } = {};
Object.keys(mapDispatchToProps).forEach((key) => {
const oldFunc = (mapDispatchToProps as any)[key];
const newFunc = (...args: any) => {
const dispatchFunc = oldFunc(...args);
if (typeof dispatchFunc === 'object') {
context.dispatch(dispatchFunc);
} else {
const result = dispatchFunc(context.dispatch)
if (typeof result === 'object' && result.then) {
result.then((dispatchObject?: DispatchObject) => {
if (dispatchObject && dispatchObject.type) {
context.dispatch(dispatchObject);
}
})
}
}
}
dispatchFuncs[key] = newFunc
});
return dispatchFuncs;
}, [mapDispatchToProps])
const props = useMemo(() => {
return Object.assign({}, ownProps, mapStateToProps(context.state, ownProps), dispatchFuncs);
}, [ownProps, context.state]);
return React.createElement<TOwnProps>(component, props);
}
return React.memo(Connect as any);
}

View File

@@ -1,73 +0,0 @@
import { Plugins } from '@capacitor/core';
import { Session } from '../models/Session';
import { Speaker } from '../models/Speaker';
import { Location } from '../models/Location';
const { Storage } = Plugins;
const locationsUrl = '/assets/data/locations.json';
const sessionsUrl = '/assets/data/sessions.json';
const speakersUrl = '/assets/data/speakers.json';
const HAS_LOGGED_IN = 'hasLoggedIn';
const HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
const USERNAME = 'username';
export const getConfData = async () => {
const response = await Promise.all([
fetch(sessionsUrl),
fetch(locationsUrl),
fetch(speakersUrl),
]);
const sessions = (await response[0].json()) as Session[];
const locations = (await response[1].json()) as Location[];
const speakers = (await response[2].json()) as Speaker[];
const allTracks = sessions
.reduce((all, session) => all.concat(session.tracks), [] as string[])
.filter((trackName, index, array) => array.indexOf(trackName) === index)
.sort();
const data = {
sessions,
locations,
speakers,
allTracks,
filteredTracks: [...allTracks],
};
return data;
};
export const getUserData = async () => {
const response = await Promise.all([
Storage.get({ key: HAS_LOGGED_IN }),
Storage.get({ key: HAS_SEEN_TUTORIAL }),
Storage.get({ key: USERNAME }),
]);
const isLoggedin = (await response[0].value) === 'true';
const hasSeenTutorial = (await response[1].value) === 'true';
const username = (await response[2].value) || undefined;
const data = {
isLoggedin,
hasSeenTutorial,
username,
};
return data;
};
export const setIsLoggedInData = async (isLoggedIn: boolean) => {
await Storage.set({ key: HAS_LOGGED_IN, value: JSON.stringify(isLoggedIn) });
};
export const setHasSeenTutorialData = async (hasSeenTutorial: boolean) => {
await Storage.set({
key: HAS_SEEN_TUTORIAL,
value: JSON.stringify(hasSeenTutorial),
});
};
export const setUsernameData = async (username?: string) => {
if (!username) {
await Storage.remove({ key: USERNAME });
} else {
await Storage.set({ key: USERNAME, value: username });
}
};

View File

@@ -1,139 +0,0 @@
import { createSelector } from 'reselect';
import { parseISO as parseDate } from 'date-fns';
import { Session } from '../models/Session';
import { SessionGroup } from '../models/SessionGroup';
import { AppState } from './state';
const getSessions = (state: AppState) => state.data.sessions;
export const getSpeakers = (state: AppState) => state.data.speakers;
const getFilteredTracks = (state: AppState) => state.data.filteredTracks;
const getFavoriteIds = (state: AppState) => state.data.favorites;
const getSearchText = (state: AppState) => state.data.searchText;
export const getFilteredSessions = createSelector(
getSessions,
getFilteredTracks,
(sessions, filteredTracks) => {
return sessions.filter(session => {
let include = false;
session.tracks.forEach(track => {
if (filteredTracks.indexOf(track) > -1) {
include = true;
}
});
return include;
});
}
);
export const getSearchedSessions = createSelector(
getFilteredSessions,
getSearchText,
(sessions, searchText) => {
if (!searchText) {
return sessions;
}
return sessions.filter(
session =>
session.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
);
}
);
export const getGroupedSessions = createSelector(
getSearchedSessions,
sessions => {
return groupSessions(sessions);
}
);
export const getFavorites = createSelector(
getSearchedSessions,
getFavoriteIds,
(sessions, favoriteIds) =>
sessions.filter(x => favoriteIds.indexOf(x.id) > -1)
);
export const getGroupedFavorites = createSelector(
getFavorites,
sessions => {
return groupSessions(sessions);
}
);
const getIdParam = (_state: AppState, props: any) => {
const stringParam = props.match.params['id'];
return parseInt(stringParam, 10);
};
export const getSession = createSelector(
getSessions,
getIdParam,
(sessions, id) => sessions.find(x => x.id === id)
);
function groupSessions(sessions: Session[]) {
return sessions
.sort(
(a, b) =>
parseDate(a.dateTimeStart).valueOf() -
parseDate(b.dateTimeStart).valueOf()
)
.reduce(
(groups, session) => {
let starterHour = parseDate(session.dateTimeStart);
starterHour.setMinutes(0);
starterHour.setSeconds(0);
const starterHourStr = starterHour.toJSON();
const foundGroup = groups.find(
group => group.startTime === starterHourStr
);
if (foundGroup) {
foundGroup.sessions.push(session);
} else {
groups.push({
startTime: starterHourStr,
sessions: [session],
});
}
return groups;
},
[] as SessionGroup[]
);
}
export const getSpeaker = createSelector(
getSpeakers,
getIdParam,
(speakers, id) => speakers.find(x => x.id === id)
);
export const getSpeakerSessions = createSelector(
getSessions,
sessions => {
const speakerSessions: { [key: number]: Session[] } = {};
sessions.forEach(session => {
session.speakerIds.forEach(speakerId => {
if (speakerSessions[speakerId]) {
speakerSessions[speakerId].push(session);
} else {
speakerSessions[speakerId] = [session];
}
});
});
return speakerSessions;
}
);
export const mapCenter = (state: AppState) => {
const item = state.data.locations.find(l => l.id === state.data.mapCenterId);
if (item == null) {
return {
id: 1,
name: 'Map Center',
lat: 43.071584,
lng: -89.38012,
};
}
return item;
};

View File

@@ -1,54 +0,0 @@
import { getConfData } from '../dataApi';
import { ActionType } from '../../util/types';
import { SessionsState } from './sessions.state';
export const loadConfData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
const data = await getConfData();
dispatch(setData(data));
dispatch(setLoading(false));
};
export const setLoading = (isLoading: boolean) =>
({
type: 'set-conf-loading',
isLoading,
} as const);
export const setData = (data: Partial<SessionsState>) =>
({
type: 'set-conf-data',
data,
} as const);
export const addFavorite = (sessionId: number) =>
({
type: 'add-favorite',
sessionId,
} as const);
export const removeFavorite = (sessionId: number) =>
({
type: 'remove-favorite',
sessionId,
} as const);
export const updateFilteredTracks = (filteredTracks: string[]) =>
({
type: 'update-filtered-tracks',
filteredTracks,
} as const);
export const setSearchText = (searchText?: string) =>
({
type: 'set-search-text',
searchText,
} as const);
export type SessionsActions =
| ActionType<typeof setLoading>
| ActionType<typeof setData>
| ActionType<typeof addFavorite>
| ActionType<typeof removeFavorite>
| ActionType<typeof updateFilteredTracks>
| ActionType<typeof setSearchText>;

View File

@@ -1,31 +0,0 @@
import { SessionsActions } from './sessions.actions';
import { SessionsState } from './sessions.state';
export const sessionsReducer = (
state: SessionsState,
action: SessionsActions
): SessionsState => {
switch (action.type) {
case 'set-conf-loading': {
return { ...state, loading: action.isLoading };
}
case 'set-conf-data': {
return { ...state, ...action.data };
}
case 'add-favorite': {
return { ...state, favorites: [...state.favorites, action.sessionId] };
}
case 'remove-favorite': {
return {
...state,
favorites: [...state.favorites.filter(x => x !== action.sessionId)],
};
}
case 'update-filtered-tracks': {
return { ...state, filteredTracks: action.filteredTracks };
}
case 'set-search-text': {
return { ...state, searchText: action.searchText };
}
}
};

View File

@@ -1,14 +0,0 @@
import { Location } from '../../models/Location';
import { Speaker } from '../../models/Speaker';
import { Session } from '../../models/Session';
export interface SessionsState {
sessions: Session[];
speakers: Speaker[];
favorites: number[];
locations: Location[];
filteredTracks: string[];
searchText?: string;
mapCenterId?: number;
loading?: boolean;
allTracks: string[];
}

View File

@@ -1,29 +0,0 @@
import { combineReducers } from './combineReducers';
import { sessionsReducer } from './sessions/sessions.reducer';
import { userReducer } from './user/user.reducer';
export const initialState: AppState = {
data: {
sessions: [],
speakers: [],
favorites: [],
locations: [],
allTracks: [],
filteredTracks: [],
mapCenterId: 0,
loading: false,
},
user: {
hasSeenTutorial: false,
darkMode: false,
isLoggedin: false,
loading: false,
},
};
export const reducers = combineReducers({
data: sessionsReducer,
user: userReducer,
});
export type AppState = ReturnType<typeof reducers>;

View File

@@ -1,76 +0,0 @@
import {
getUserData,
setIsLoggedInData,
setUsernameData,
setHasSeenTutorialData,
} from '../dataApi';
import { ActionType } from '../../util/types';
import { UserState } from './user.state';
export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
const data = await getUserData();
dispatch(setData(data));
dispatch(setLoading(false));
};
export const setLoading = (isLoading: boolean) =>
({
type: 'set-user-loading',
isLoading,
} as const);
export const setData = (data: Partial<UserState>) =>
({
type: 'set-user-data',
data,
} as const);
export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(false);
dispatch(setUsername());
};
export const setIsLoggedIn = (loggedIn: boolean) => async (
dispatch: React.Dispatch<any>
) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setUsername = (username?: string) => async (
dispatch: React.Dispatch<any>
) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setHasSeenTutorial = (hasSeenTutorial: boolean) => async (
dispatch: React.Dispatch<any>
) => {
await setHasSeenTutorialData(hasSeenTutorial);
return {
type: 'set-has-seen-tutorial',
hasSeenTutorial,
} as const;
};
export const setDarkMode = (darkMode: boolean) =>
({
type: 'set-dark-mode',
darkMode,
} as const);
export type UserActions =
| ActionType<typeof setLoading>
| ActionType<typeof setData>
| ActionType<typeof setIsLoggedIn>
| ActionType<typeof setUsername>
| ActionType<typeof setHasSeenTutorial>
| ActionType<typeof setDarkMode>;

View File

@@ -1,19 +0,0 @@
import { UserActions } from './user.actions';
import { UserState } from './user.state';
export function userReducer(state: UserState, action: UserActions): UserState {
switch (action.type) {
case 'set-user-loading':
return { ...state, loading: action.isLoading };
case 'set-user-data':
return { ...state, ...action.data };
case 'set-username':
return { ...state, username: action.username };
case 'set-has-seen-tutorial':
return { ...state, hasSeenTutorial: action.hasSeenTutorial };
case 'set-dark-mode':
return { ...state, darkMode: action.darkMode };
case 'set-is-loggedin':
return { ...state, isLoggedin: action.loggedIn };
}
}

View File

@@ -1,7 +0,0 @@
export interface UserState {
isLoggedin: boolean;
username?: string;
darkMode: boolean;
hasSeenTutorial: boolean;
loading: boolean;
}

View File

@@ -1,5 +0,0 @@
export interface AppPage {
url: string;
icon: object;
title: string;
}

View File

@@ -1,5 +1,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root')); ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View File

@@ -1,6 +0,0 @@
export interface Location {
id: number;
name?: string;
lat: number;
lng: number;
}

View File

@@ -1,10 +0,0 @@
export interface Session {
id: number;
dateTimeStart: string;
dateTimeEnd: string;
name: string;
location: string;
description: string;
speakerIds: number[];
tracks: string[];
}

View File

@@ -1,5 +0,0 @@
import { Session } from './Session';
export interface SessionGroup {
startTime: string;
sessions: Session[];
}

View File

@@ -1,10 +0,0 @@
export interface Speaker {
id: number;
name: string;
profilePic: string;
twitter: string;
about: string;
location: string;
email: string;
phone: string;
}

View File

@@ -1,26 +0,0 @@
#about-page {
.about-header {
background-color: #222;
padding: 16px;
width: 100%;
height: 30%;
text-align: center;
}
.about-header img {
max-height: 100%;
}
.about-info p {
color: var(--ion-color-dark);
text-align: left;
}
.about-info ion-icon {
margin-inline-end: 32px;
}
.ios .about-info {
text-align: center;
}
}

View File

@@ -1,81 +0,0 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonButton, IonIcon, IonDatetime, IonSelectOption, IonList, IonItem, IonLabel, IonSelect, IonPopover } from '@ionic/react';
import './About.scss';
import { calendar, pin, more } from 'ionicons/icons';
import AboutPopover from '../components/AboutPopover';
interface AboutProps { }
const About: React.FC<AboutProps> = () => {
const [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState();
const presentPopover = (e: React.MouseEvent) => {
setPopoverEvent(e.nativeEvent);
setShowPopover(true);
};
const conferenceDate = '2047-05-17';
return (
<IonPage id="about-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>About</IonTitle>
<IonButtons slot="end">
<IonButton icon-only onClick={presentPopover}>
<IonIcon slot="icon-only" icon={more}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="about-header">
<img src="assets/img/ionic-logo-white.svg" alt="ionic logo" />
</div>
<div className="about-info">
<h4 className="ion-padding-start">Ionic Conference</h4>
<IonList lines="none">
<IonItem>
<IonIcon icon={calendar} slot="start"></IonIcon>
<IonLabel position="stacked">Date</IonLabel>
<IonDatetime displayFormat="MMM DD, YYYY" max="2056" value={conferenceDate}></IonDatetime>
</IonItem>
<IonItem>
<IonIcon icon={pin} slot="start"></IonIcon>
<IonLabel position="stacked">Location</IonLabel>
<IonSelect>
<IonSelectOption value="madison" selected>Madison, WI</IonSelectOption>
<IonSelectOption value="austin">Austin, TX</IonSelectOption>
<IonSelectOption value="chicago">Chicago, IL</IonSelectOption>
<IonSelectOption value="seattle">Seattle, WA</IonSelectOption>
</IonSelect>
</IonItem>
</IonList>
<p className="ion-padding-start ion-padding-end">
The Ionic Conference is a one-day conference featuring talks from the Ionic team. It is focused on Ionic applications being
built with Ionic 2. This includes migrating apps from Ionic 1 to Ionic 2, Angular concepts, Webpack, Sass, and many
other technologies used in Ionic 2. Tickets are completely sold out, and were expecting more than 1000 developers
making this the largest Ionic conference ever!
</p>
</div>
</IonContent>
<IonPopover
isOpen={showPopover}
event={popoverEvent}
onDidDismiss={() => setShowPopover(false)}
>
<AboutPopover dismiss={() => setShowPopover(false)} />
</IonPopover>
</IonPage>
);
};
export default React.memo(About);

View File

@@ -1,6 +0,0 @@
#account-page {
img {
max-width: 140px;
border-radius: 50%;
}
}

View File

@@ -1,87 +0,0 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonList, IonItem, IonAlert } from '@ionic/react';
import './Account.scss';
import { setUsername } from '../data/user/user.actions';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps { }
interface StateProps {
username?: string;
}
interface DispatchProps {
setUsername: typeof setUsername;
}
interface AccountProps extends OwnProps, StateProps, DispatchProps { }
const Account: React.FC<AccountProps> = ({ setUsername, username }) => {
const [showAlert, setShowAlert] = useState(false);
const clicked = (text: string) => {
console.log(`Clicked ${text}`);
}
return (
<IonPage id="account-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Account</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{username &&
(<div className="ion-padding-top ion-text-center">
<img src="https://www.gravatar.com/avatar?d=mm&s=140" alt="avatar" />
<h2>{ username }</h2>
<IonList inset>
<IonItem onClick={() => clicked('Update Picture')}>Update Picture</IonItem>
<IonItem onClick={() => setShowAlert(true)}>Change Username</IonItem>
<IonItem onClick={() => clicked('Change Password')}>Change Password</IonItem>
<IonItem routerLink="/support" routerDirection="none">Support</IonItem>
<IonItem routerLink="/logout" routerDirection="none">Logout</IonItem>
</IonList>
</div>)
}
</IonContent>
<IonAlert
isOpen={showAlert}
header="Change Username"
buttons={[
'Cancel',
{
text: 'Ok',
handler: (data) => {
setUsername(data.username);
}
}
]}
inputs={[
{
type: 'text',
name: 'username',
value: username,
placeholder: 'username'
}
]}
onDidDismiss={() => setShowAlert(false)}
/>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
username: state.user.username
}),
mapDispatchToProps: {
setUsername,
},
component: Account
})

View File

@@ -0,0 +1,26 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';
const Home: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Ionic Blank</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
The world is your oyster.
<p>
If you get lost, the{' '}
<a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/">
docs
</a>{' '}
will be your guide.
</p>
</IonContent>
</IonPage>
);
};
export default Home;

View File

@@ -1,16 +0,0 @@
#login-page, #signup-page, #support-page {
.login-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.login-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}
}

View File

@@ -1,108 +0,0 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonRow, IonCol, IonButton, IonList, IonItem, IonLabel, IonInput, IonText } from '@ionic/react';
import './Login.scss';
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps {}
interface DispatchProps {
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface LoginProps extends OwnProps, DispatchProps { }
const Login: React.FC<LoginProps> = ({setIsLoggedIn, history, setUsername: setUsernameAction}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [usernameError, setUsernameError] = useState(false);
const [passwordError, setPasswordError] = useState(false);
const login = async (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if(!username) {
setUsernameError(true);
}
if(!password) {
setPasswordError(true);
}
if(username && password) {
await setIsLoggedIn(true);
await setUsernameAction(username);
history.push('/tabs/schedule', {direction: 'none'});
}
};
return (
<IonPage id="login-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Login</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={login}>
<IonList>
<IonItem>
<IonLabel position="stacked" color="primary">Username</IonLabel>
<IonInput name="username" type="text" value={username} spellCheck={false} autocapitalize="off" onIonChange={e => setUsername(e.detail.value!)}
required>
</IonInput>
</IonItem>
{formSubmitted && usernameError && <IonText color="danger">
<p className="ion-padding-start">
Username is required
</p>
</IonText>}
<IonItem>
<IonLabel position="stacked" color="primary">Password</IonLabel>
<IonInput name="password" type="password" value={password} onIonChange={e => setPassword(e.detail.value!)}>
</IonInput>
</IonItem>
{formSubmitted && passwordError && <IonText color="danger">
<p className="ion-padding-start">
Password is required
</p>
</IonText>}
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">Login</IonButton>
</IonCol>
<IonCol>
<IonButton routerLink="/signup" color="light" expand="block">Signup</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: {
setIsLoggedIn,
setUsername
},
component: Login
})

View File

@@ -1,54 +0,0 @@
import React from 'react';
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { calendar, contacts, map, informationCircle } from 'ionicons/icons';
import SchedulePage from './SchedulePage';
import SpeakerList from './SpeakerList';
import SpeakerDetail from './SpeakerDetail';
import SessionDetail from './SessionDetail';
import MapView from './MapView';
import About from './About';
interface MainTabsProps { }
const MainTabs: React.FC<MainTabsProps> = () => {
return (
<IonTabs>
<IonRouterOutlet>
<Redirect exact path="/tabs" to="/tabs/schedule" />
{/*
Using the render method prop cuts down the number of renders your components will have due to route changes.
Use the component prop when your component depends on the RouterComponentProps passed in automatically.
*/}
<Route path="/tabs/schedule" render={() => <SchedulePage />} exact={true} />
<Route path="/tabs/speakers" render={() => <SpeakerList />} exact={true} />
<Route path="/tabs/speakers/:id" component={SpeakerDetail} exact={true} />
<Route path="/tabs/schedule/:id" component={SessionDetail} />
<Route path="/tabs/speakers/sessions/:id" component={SessionDetail} />
<Route path="/tabs/map" render={() => <MapView />} exact={true} />
<Route path="/tabs/about" render={() => <About />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="schedule" href="/tabs/schedule">
<IonIcon icon={calendar} />
<IonLabel>Schedule</IonLabel>
</IonTabButton>
<IonTabButton tab="speakers" href="/tabs/speakers">
<IonIcon icon={contacts} />
<IonLabel>Speakers</IonLabel>
</IonTabButton>
<IonTabButton tab="map" href="/tabs/map">
<IonIcon icon={map} />
<IonLabel>Map</IonLabel>
</IonTabButton>
<IonTabButton tab="about" href="/tabs/about">
<IonIcon icon={informationCircle} />
<IonLabel>About</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default MainTabs;

View File

@@ -1,18 +0,0 @@
#map-view {
.map-canvas {
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
opacity: 0;
transition: opacity 250ms ease-in;
}
.show-map {
opacity: 1;
}
}

View File

@@ -1,44 +0,0 @@
import React from 'react';
import Map from '../components/Map';
import { IonHeader, IonToolbar, IonButtons, IonMenuButton, IonTitle, IonContent, IonPage } from '@ionic/react';
import { Location } from '../models/Location';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import './MapView.scss';
interface OwnProps { }
interface StateProps {
locations: Location[];
mapCenter: Location;
}
interface DispatchProps { }
interface MapViewProps extends OwnProps, StateProps, DispatchProps { };
const MapView: React.FC<MapViewProps> = ({ locations, mapCenter }) => {
return (
<IonPage id="map-view">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Map</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="map-page">
<Map locations={locations} mapCenter={mapCenter} />
</IonContent>
</IonPage>
)};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
locations: state.data.locations,
mapCenter: selectors.mapCenter(state)
}),
component: MapView
});

View File

@@ -1,42 +0,0 @@
#schedule-page {
ion-item-sliding.track-ionic ion-label {
border-left: 2px solid var(--ion-color-primary);
padding-left: 10px;
}
ion-item-sliding.track-angular ion-label {
border-left: 2px solid var(--ion-color-angular);
padding-left: 10px;
}
ion-item-sliding.track-communication ion-label {
border-left: 2px solid var(--ion-color-communication);
padding-left: 10px;
}
ion-item-sliding.track-tooling ion-label {
border-left: 2px solid var(--ion-color-tooling);
padding-left: 10px;
}
ion-item-sliding.track-services ion-label {
border-left: 2px solid var(--ion-color-services);
padding-left: 10px;
}
ion-item-sliding.track-design ion-label {
border-left: 2px solid var(--ion-color-design);
padding-left: 10px;
}
ion-item-sliding.track-workshop ion-label {
border-left: 2px solid var(--ion-color-workshop);
padding-left: 10px;
}
ion-item-sliding.track-food ion-label {
border-left: 2px solid var(--ion-color-food);
padding-left: 10px;
}
ion-item-sliding.track-documentation ion-label {
border-left: 2px solid var(--ion-color-documentation);
padding-left: 10px;
}
ion-item-sliding.track-navigation ion-label {
border-left: 2px solid var(--ion-color-navigation);
padding-left: 10px;
}
}

View File

@@ -1,120 +0,0 @@
import React, { useState, useRef } from 'react';
import { IonToolbar, IonContent, IonPage, IonButtons, IonMenuButton, IonSegment, IonSegmentButton, IonButton, IonIcon, IonSearchbar, IonRefresher, IonRefresherContent, IonToast, IonModal, IonHeader, getConfig } from '@ionic/react';
import { connect } from '../data/connect';
import { options } from 'ionicons/icons';
import SessionList from '../components/SessionList';
import SessionListFilter from '../components/SessionListFilter';
import './SchedulePage.scss'
import * as selectors from '../data/selectors';
import { setSearchText, addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
import ShareSocialFab from '../components/ShareSocialFab';
import { SessionGroup } from '../models/SessionGroup';
interface OwnProps { }
interface StateProps {
sessionGroups: SessionGroup[];
favoriteGroups: SessionGroup[];
mode: 'ios' | 'md'
}
interface DispatchProps {
setSearchText: typeof setSearchText;
}
type SchedulePageProps = OwnProps & StateProps & DispatchProps;
const SchedulePage: React.FC<SchedulePageProps> = ({ favoriteGroups, sessionGroups, setSearchText, mode }) => {
const [segment, setSegment] = useState<'all' | 'favorites'>('all');
const [showFilterModal, setShowFilterModal] = useState(false);
const ionRefresherRef = useRef<HTMLIonRefresherElement>(null);
const [showCompleteToast, setShowCompleteToast] = useState(false);
const doRefresh = () => {
setTimeout(() => {
ionRefresherRef.current!.complete();
setShowCompleteToast(true);
}, 2500)
};
return (
<IonPage id="schedule-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonSegment onIonChange={(e) => setSegment(e.detail.value as any)}>
<IonSegmentButton value="all" checked={segment === 'all'}>
All
</IonSegmentButton>
<IonSegmentButton value="favorites" checked={segment === 'favorites'}>
Favorites
</IonSegmentButton>
</IonSegment>
<IonButtons slot="end">
<IonButton onClick={() => setShowFilterModal(true)}>
{mode === 'ios' ? 'Filter' : <IonIcon icon={options} slot="icon-only" />}
</IonButton>
</IonButtons>
</IonToolbar>
<IonToolbar>
<IonSearchbar
placeholder="Search"
onIonChange={(e: CustomEvent) => setSearchText(e.detail.value)}
/>
</IonToolbar>
</IonHeader>
<IonContent>
<IonRefresher slot="fixed" ref={ionRefresherRef} onIonRefresh={doRefresh}>
<IonRefresherContent />
</IonRefresher>
<IonToast
isOpen={showCompleteToast}
message="Refresh complete"
duration={2000}
onDidDismiss={() => setShowCompleteToast(false)}
/>
<SessionList
sessionGroups={sessionGroups}
listType={segment}
hide={segment === 'favorites'}
/>
<SessionList
sessionGroups={favoriteGroups}
listType={segment}
hide={segment === 'all'}
/>
</IonContent>
<IonModal
isOpen={showFilterModal}
onDidDismiss={() => setShowFilterModal(false)}
>
<SessionListFilter
onDismissModal={() => setShowFilterModal(false)}
/>
</IonModal>
<ShareSocialFab />
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
sessionGroups: selectors.getGroupedSessions(state),
favoriteGroups: selectors.getGroupedFavorites(state),
mode: getConfig()!.get('mode')
}),
mapDispatchToProps: {
setSearchText
},
component: React.memo(SchedulePage)
});

View File

@@ -1,73 +0,0 @@
#session-detail-page {
.session-track-ionic {
color: var(--ion-color-primary);
}
.session-track-angular {
color: var(--ion-color-angular);
}
.session-track-communication {
color: var(--ion-color-communication);
}
.session-track-tooling {
color: var(--ion-color-tooling);
}
.session-track-services {
color: var(--ion-color-services);
}
.session-track-design {
color: var(--ion-color-design);
}
.session-track-workshop {
color: var(--ion-color-workshop);
}
.session-track-food {
color: var(--ion-color-food);
}
.session-track-documentation {
color: var(--ion-color-documentation);
}
.session-track-navigation {
color: var(--ion-color-navigation);
}
.show-favorite {
position: relative;
}
.icon-heart-empty {
position: absolute;
top: 5px;
right: 5px;
transform: scale(1);
transition: transform 0.3s ease;
}
.icon-heart {
position: absolute;
top: 5px;
right: 5px;
transform: scale(0);
transition: transform 0.3s ease;
}
.show-favorite .icon-heart {
transform: scale(1);
}
.show-favorite .icon-heart-empty {
transform: scale(0);
}
h1 {
margin: 0;
}
}

View File

@@ -1,108 +0,0 @@
import React from 'react';
import { IonHeader, IonToolbar, IonContent, IonPage, IonButtons, IonBackButton, IonButton, IonIcon, IonText, IonList, IonItem, IonLabel } from '@ionic/react';
import { connect } from '../data/connect';
import { withRouter, RouteComponentProps } from 'react-router';
import * as selectors from '../data/selectors';
import { starOutline, star, share, cloudDownload } from 'ionicons/icons';
import './SessionDetail.scss';
import { Time } from '../components/Time';
import { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
import { Session } from '../models/Session';
interface OwnProps extends RouteComponentProps { };
interface StateProps {
session?: Session;
favoriteSessions: number[],
};
interface DispatchProps {
addFavorite: typeof addFavorite;
removeFavorite: typeof removeFavorite;
}
type SessionDetailProps = OwnProps & StateProps & DispatchProps;
const SessionDetail: React.FC<SessionDetailProps> = ({ session, addFavorite, removeFavorite, favoriteSessions }) => {
if (!session) {
return <div>Session not found</div>
}
const isFavorite = favoriteSessions.indexOf(session.id) > -1;
const toggleFavorite = () => {
isFavorite ? removeFavorite(session.id) : addFavorite(session.id);
};
const shareSession = () => { };
const sessionClick = (text: string) => {
console.log(`Clicked ${text}`);
};
return (
<IonPage id="session-detail-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/tabs/schedule"></IonBackButton>
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={() => toggleFavorite()}>
{isFavorite ?
<IonIcon slot="icon-only" icon={star}></IonIcon> :
<IonIcon slot="icon-only" icon={starOutline}></IonIcon>
}
</IonButton>
<IonButton onClick={() => shareSession}>
<IonIcon slot="icon-only" icon={share}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="ion-padding">
<h1>{session.name}</h1>
{session.tracks.map(track => (
<span key={track} className={`session-track-${track.toLowerCase()}`}>{track}</span>
))}
<p>{session.description}</p>
<IonText color="medium">
<Time date={session.dateTimeStart} /> &ndash; <Time date={session.dateTimeEnd} />
<br />
{session.location}
</IonText>
</div>
<IonList>
<IonItem onClick={() => sessionClick('watch')} button>
<IonLabel color="primary">Watch</IonLabel>
</IonItem>
<IonItem onClick={() => sessionClick('add to calendar')} button>
<IonLabel color="primary">Add to Calendar</IonLabel>
</IonItem>
<IonItem onClick={() => sessionClick('mark as unwatched')} button>
<IonLabel color="primary">Mark as Unwatched</IonLabel>
</IonItem>
<IonItem onClick={() => sessionClick('download video')} button>
<IonLabel color="primary">Download Video</IonLabel>
<IonIcon slot="end" color="primary" size="small" icon={cloudDownload}></IonIcon>
</IonItem>
<IonItem onClick={() => sessionClick('leave feedback')} button>
<IonLabel color="primary">Leave Feedback</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state, OwnProps) => ({
session: selectors.getSession(state, OwnProps),
favoriteSessions: state.data.favorites
}),
mapDispatchToProps: {
addFavorite,
removeFavorite
},
component: withRouter(SessionDetail)
})

View File

@@ -1,111 +0,0 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonRow, IonCol, IonButton, IonList, IonItem, IonLabel, IonInput, IonText } from '@ionic/react';
import './Login.scss';
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps {}
interface DispatchProps {
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface LoginProps extends OwnProps, DispatchProps { }
const Login: React.FC<LoginProps> = ({setIsLoggedIn, history, setUsername: setUsernameAction}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [usernameError, setUsernameError] = useState(false);
const [passwordError, setPasswordError] = useState(false);
const login = async (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if(!username) {
setUsernameError(true);
}
if(!password) {
setPasswordError(true);
}
if(username && password) {
await setIsLoggedIn(true);
await setUsernameAction(username);
history.push('/tabs/schedule', {direction: 'none'});
}
};
return (
<IonPage id="signup-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Signup</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={login}>
<IonList>
<IonItem>
<IonLabel position="stacked" color="primary">Username</IonLabel>
<IonInput name="username" type="text" value={username} spellCheck={false} autocapitalize="off" onIonChange={e => {
setUsername(e.detail.value!);
setUsernameError(false);
}}
required>
</IonInput>
</IonItem>
{formSubmitted && usernameError && <IonText color="danger">
<p className="ion-padding-start">
Username is required
</p>
</IonText>}
<IonItem>
<IonLabel position="stacked" color="primary">Password</IonLabel>
<IonInput name="password" type="password" value={password} onIonChange={e => {
setPassword(e.detail.value!);
setPasswordError(false);
}}>
</IonInput>
</IonItem>
{formSubmitted && passwordError && <IonText color="danger">
<p className="ion-padding-start">
Password is required
</p>
</IonText>}
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">Create</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: {
setIsLoggedIn,
setUsername
},
component: Login
})

View File

@@ -1,11 +0,0 @@
#speaker-detail {
img {
max-width: 140px;
border-radius: 50%;
}
p {
color: #60646B;
}
}

View File

@@ -1,64 +0,0 @@
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { IonIcon, IonHeader, IonToolbar, IonButtons, IonTitle, IonContent, IonButton, IonBackButton, IonPage } from '@ionic/react'
import './SpeakerDetail.scss';
import { logoTwitter, logoGithub, logoInstagram } from 'ionicons/icons';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import { Speaker } from '../models/Speaker';
interface OwnProps extends RouteComponentProps {
speaker?: Speaker;
};
interface StateProps {};
interface DispatchProps {};
interface SpeakerDetailProps extends OwnProps, StateProps, DispatchProps {};
const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
if (!speaker) {
return <div>Speaker not found</div>
}
return (
<IonPage id="speaker-detail">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/tabs/speakers" />
</IonButtons>
<IonTitle>{speaker.name}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding speaker-detail speaker-page-list">
<div className="ion-text-center">
<img src={speaker.profilePic} alt={speaker.name} />
<br />
<IonButton fill="clear" size="small" color="twitter">
<IonIcon icon={logoTwitter} slot="icon-only"></IonIcon>
</IonButton>
<IonButton fill="clear" size="small" color="github">
<IonIcon icon={logoGithub} slot="icon-only"></IonIcon>
</IonButton>
<IonButton fill="clear" size="small" color="instagram">
<IonIcon icon={logoInstagram} slot="icon-only"></IonIcon>
</IonButton>
</div>
<p>{speaker.about}</p>
</IonContent>
</IonPage>
);
};
export default connect({
mapStateToProps: (state, ownProps) => ({
speaker: selectors.getSpeaker(state, ownProps)
}),
component: SpeakerDetail
});

View File

@@ -1,24 +0,0 @@
#speaker-list {
.scroll {
background: #ededed;
}
.speaker-card {
height: 100%;
display: flex;
flex-direction: column;
}
.speaker-card ion-card-header {
padding: 0;
}
.speaker-card ion-card-header .item {
padding: 4px 16px;
}
.speaker-card ion-card-content {
flex: 1 1 auto;
padding: 0;
}
}

View File

@@ -1,61 +0,0 @@
import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonList, IonGrid, IonRow, IonCol } from '@ionic/react';
import SpeakerItem from '../components/SpeakerItem';
import { Speaker } from '../models/Speaker';
import { Session } from '../models/Session';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import './SpeakerList.scss';
interface OwnProps { };
interface StateProps {
speakers: Speaker[];
speakerSessions: { [key: number]: Session[] };
};
interface DispatchProps { };
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps { };
const SpeakerList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) => {
return (
<IonPage id="speaker-list">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Speakers</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className={`outer-content`}>
<IonList>
<IonGrid fixed>
<IonRow align-items-stretch>
{speakers.map(speaker => (
<IonCol size="12" size-md="6" key={speaker.id}>
<SpeakerItem
key={speaker.id}
speaker={speaker}
sessions={speakerSessions[speaker.id]}
/>
</IonCol>
))}
</IonRow>
</IonGrid>
</IonList>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
speakers: selectors.getSpeakers(state),
speakerSessions: selectors.getSpeakerSessions(state)
}),
component: React.memo(SpeakerList)
});

View File

@@ -1,83 +0,0 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonRow, IonCol, IonButton, IonList, IonItem, IonLabel, IonText, IonTextarea, IonToast } from '@ionic/react';
import './Login.scss';
import { connect } from '../data/connect';
interface OwnProps { }
interface DispatchProps { }
interface SupportProps extends OwnProps, DispatchProps { }
const Support: React.FC<SupportProps> = () => {
const [message, setMessage] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [messageError, setMessageError] = useState(false);
const [showToast, setShowToast] = useState(false);
const send = (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if (!message) {
setMessageError(true);
}
if (message) {
setMessage('');
setShowToast(true);
}
};
return (
<IonPage id="support-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Support</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={send}>
<IonList>
<IonItem>
<IonLabel position="stacked" color="primary">Enter your support message below</IonLabel>
<IonTextarea name="message" value={message} spellCheck={false} autocapitalize="off" rows={6} onIonChange={e => setMessage(e.detail.value!)}
required>
</IonTextarea>
</IonItem>
{formSubmitted && messageError && <IonText color="danger">
<p className="ion-padding-start">
Support message is required
</p>
</IonText>}
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">Submit</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
<IonToast
isOpen={showToast}
duration={3000}
message="Your support request has been sent"
onDidDismiss={() => setShowToast(false)} />
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
component: Support
})

View File

@@ -1,38 +0,0 @@
#tutorial-page {
ion-toolbar {
// TODO test transparent and fullscreen
--background: transparent;
--border-color: transparent;
}
.swiper-slide {
display: block;
}
.slide-title {
margin-top: 2.8rem;
}
.slide-image {
max-height: 50%;
max-width: 60%;
margin: 36px 0;
pointer-events: none;
}
b {
font-weight: 500;
}
p {
padding: 0 40px;
font-size: 14px;
line-height: 1.5;
color: var(--ion-color-step-600, #60646b);
b {
color: var(--ion-text-color, #000000);
}
}
}

View File

@@ -1,87 +0,0 @@
import React, { useState, useRef } from 'react';
import { IonContent, IonPage, IonHeader, IonToolbar, IonButtons, IonButton, IonSlides, IonSlide, IonIcon } from '@ionic/react';
import { arrowForward } from 'ionicons/icons';
import { setHasSeenTutorial } from '../data/user/user.actions';
import './Tutorial.scss';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps {};
interface DispatchProps {
setHasSeenTutorial: typeof setHasSeenTutorial
}
interface TutorialProps extends OwnProps, DispatchProps { };
const Tutorial: React.FC<TutorialProps> = ({ history, setHasSeenTutorial }) => {
const [showSkip, setShowSkip] = useState(true);
const slideRef = useRef<HTMLIonSlidesElement>(null);
const startApp = async () => {
await setHasSeenTutorial(true);
history.push('/tabs/schedule', { direction: 'none' });
};
const handleSlideChangeStart = () => {
slideRef.current!.isEnd().then(isEnd => setShowSkip(!isEnd));
};
return (
<IonPage id="tutorial-page">
<IonHeader no-border>
<IonToolbar>
<IonButtons slot="end">
{showSkip && <IonButton color='primary' onClick={startApp}>Skip</IonButton>}
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonSlides ref={slideRef} onIonSlideWillChange={handleSlideChangeStart} pager={false}>
<IonSlide>
<img src="assets/img/ica-slidebox-img-1.png" alt="" className="slide-image" />
<h2 className="slide-title">
Welcome to <b>ICA</b>
</h2>
<p>
The <b>ionic conference app</b> is a practical preview of the ionic framework in action, and a demonstration of proper code use.
</p>
</IonSlide>
<IonSlide>
<img src="assets/img/ica-slidebox-img-2.png" alt="" className="slide-image" />
<h2 className="slide-title">What is Ionic?</h2>
<p>
<b>Ionic Framework</b> is an open source SDK that enables developers to build high quality mobile apps with web technologies like HTML, CSS, and JavaScript.
</p>
</IonSlide>
<IonSlide>
<img src="assets/img/ica-slidebox-img-3.png" alt="" className="slide-image" />
<h2 className="slide-title">What is Ionic Appflow?</h2>
<p>
<b>Ionic Appflow</b> is a powerful set of services and features built on top of Ionic Framework that brings a totally new level of app development agility to mobile dev teams.
</p>
</IonSlide>
<IonSlide>
<img src="assets/img/ica-slidebox-img-4.png" alt="" className="slide-image" />
<h2 className="slide-title">Ready to Play?</h2>
<IonButton fill="clear" onClick={startApp}>
Continue
<IonIcon slot="end" icon={arrowForward} />
</IonButton>
</IonSlide>
</IonSlides>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: ({
setHasSeenTutorial
}),
component: Tutorial
});

View File

@@ -0,0 +1,145 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View File

@@ -1,179 +0,0 @@
/* Ionic Variables and Theming. For more information, please see
// https://beta.ionicframework.com/docs/theming/
// The app direction is used to include
// rtl styles in your app. For more information, please see
// https://beta.ionicframework.com/docs/layout/rtl
// $app-direction: ltr;
// Ionic Colors
// --------------------------------------------------
// Named colors makes it easy to reuse colors on various components.
// It's highly recommended to change the default colors
// to match your app's branding. Ionic provides eight layered colors
// that can be changed to theme an app. Additional colors can be
// added as well (see below). For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
// To easily create custom color palettes for your apps UI,
// check out our color generator:
// https://beta.ionicframework.com/docs/theming/color-generator
*/
:root {
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
--ion-color-secondary: #0cd1e8;
--ion-color-secondary-rgb: 12, 209, 232;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #0bb8cc;
--ion-color-secondary-tint: #24d6ea;
--ion-color-tertiary: #7044ff;
--ion-color-tertiary-rgb: 112, 68, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #633ce0;
--ion-color-tertiary-tint: #7e57ff;
--ion-color-success: #10dc60;
--ion-color-success-rgb: 16, 220, 96;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #0ec254;
--ion-color-success-tint: #28e070;
--ion-color-warning: #ffce00;
--ion-color-warning-rgb: 255, 206, 0;
--ion-color-warning-contrast: #ffffff;
--ion-color-warning-contrast-rgb: 255, 255, 255;
--ion-color-warning-shade: #e0b500;
--ion-color-warning-tint: #ffd31a;
--ion-color-danger: #f04141;
--ion-color-danger-rgb: 245, 61, 61;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #d33939;
--ion-color-danger-tint: #f25454;
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 34, 34;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 244, 244;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
/* Additional Ionic Colors
// --------------------------------------------------
// In order to add colors to be used with Ionic components,
// the color should be added as a class with the convention `.ion-color-{COLOR}`
// where `{COLOR}` is the color to be used on the Ionic component
// and each variant is defined for the color. For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
*/
.ion-color-favorite {
--ion-color-base: #69bb7b;
--ion-color-base-rgb: 105, 187, 123;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #5ca56c;
--ion-color-tint: #78c288;
}
.ion-color-twitter {
--ion-color-base: #1da1f4;
--ion-color-base-rgb: 29, 161, 244;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1a8ed7;
--ion-color-tint: #34aaf5;
}
.ion-color-google {
--ion-color-base: #dc4a38;
--ion-color-base-rgb: 220, 74, 56;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #c24131;
--ion-color-tint: #e05c4c;
}
.ion-color-vimeo {
--ion-color-base: #23b6ea;
--ion-color-base-rgb: 35, 182, 234;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1fa0ce;
--ion-color-tint: #39bdec;
}
.ion-color-facebook {
--ion-color-base: #3b5998;
--ion-color-base-rgb: 59, 89, 152;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #344e86;
--ion-color-tint: #4f6aa2;
}
/* Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the CSS variables found in Ionic's source files.
// To view all the possible Ionic variables, see:
// https://beta.ionicframework.com/docs/theming/css-variables#ionic-variables
*/
:root {
--ion-headings-font-weight: 300;
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
}
.md {
--ion-toolbar-background: var(--ion-color-primary);
--ion-toolbar-color: #fff;
--ion-toolbar-color-activated: #fff;
}

View File

@@ -1,34 +1,9 @@
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
/* Ionic Variables and Theming. For more information, please see /** Ionic CSS Variables **/
// https://beta.ionicframework.com/docs/theming/
// The app direction is used to include
// rtl styles in your app. For more information, please see
// https://beta.ionicframework.com/docs/layout/rtl
// $app-direction: ltr;
// Ionic Colors
// --------------------------------------------------
// Named colors makes it easy to reuse colors on various components.
// It's highly recommended to change the default colors
// to match your app's branding. Ionic provides eight layered colors
// that can be changed to theme an app. Additional colors can be
// added as well (see below). For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
// To easily create custom color palettes for your apps UI,
// check out our color generator:
// https://beta.ionicframework.com/docs/theming/color-generator
*/
:root { :root {
--ion-color-angular: #ac282b; /** primary **/
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
--ion-color-primary: #3880ff; --ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255; --ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff; --ion-color-primary-contrast: #ffffff;
@@ -36,6 +11,7 @@
--ion-color-primary-shade: #3171e0; --ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff; --ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #0cd1e8; --ion-color-secondary: #0cd1e8;
--ion-color-secondary-rgb: 12, 209, 232; --ion-color-secondary-rgb: 12, 209, 232;
--ion-color-secondary-contrast: #ffffff; --ion-color-secondary-contrast: #ffffff;
@@ -43,6 +19,7 @@
--ion-color-secondary-shade: #0bb8cc; --ion-color-secondary-shade: #0bb8cc;
--ion-color-secondary-tint: #24d6ea; --ion-color-secondary-tint: #24d6ea;
/** tertiary **/
--ion-color-tertiary: #7044ff; --ion-color-tertiary: #7044ff;
--ion-color-tertiary-rgb: 112, 68, 255; --ion-color-tertiary-rgb: 112, 68, 255;
--ion-color-tertiary-contrast: #ffffff; --ion-color-tertiary-contrast: #ffffff;
@@ -50,6 +27,7 @@
--ion-color-tertiary-shade: #633ce0; --ion-color-tertiary-shade: #633ce0;
--ion-color-tertiary-tint: #7e57ff; --ion-color-tertiary-tint: #7e57ff;
/** success **/
--ion-color-success: #10dc60; --ion-color-success: #10dc60;
--ion-color-success-rgb: 16, 220, 96; --ion-color-success-rgb: 16, 220, 96;
--ion-color-success-contrast: #ffffff; --ion-color-success-contrast: #ffffff;
@@ -57,6 +35,7 @@
--ion-color-success-shade: #0ec254; --ion-color-success-shade: #0ec254;
--ion-color-success-tint: #28e070; --ion-color-success-tint: #28e070;
/** warning **/
--ion-color-warning: #ffce00; --ion-color-warning: #ffce00;
--ion-color-warning-rgb: 255, 206, 0; --ion-color-warning-rgb: 255, 206, 0;
--ion-color-warning-contrast: #ffffff; --ion-color-warning-contrast: #ffffff;
@@ -64,6 +43,7 @@
--ion-color-warning-shade: #e0b500; --ion-color-warning-shade: #e0b500;
--ion-color-warning-tint: #ffd31a; --ion-color-warning-tint: #ffd31a;
/** danger **/
--ion-color-danger: #f04141; --ion-color-danger: #f04141;
--ion-color-danger-rgb: 245, 61, 61; --ion-color-danger-rgb: 245, 61, 61;
--ion-color-danger-contrast: #ffffff; --ion-color-danger-contrast: #ffffff;
@@ -71,6 +51,7 @@
--ion-color-danger-shade: #d33939; --ion-color-danger-shade: #d33939;
--ion-color-danger-tint: #f25454; --ion-color-danger-tint: #f25454;
/** dark **/
--ion-color-dark: #222428; --ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 34, 34; --ion-color-dark-rgb: 34, 34, 34;
--ion-color-dark-contrast: #ffffff; --ion-color-dark-contrast: #ffffff;
@@ -78,6 +59,7 @@
--ion-color-dark-shade: #1e2023; --ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e; --ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #989aa2; --ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162; --ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #ffffff; --ion-color-medium-contrast: #ffffff;
@@ -85,6 +67,7 @@
--ion-color-medium-shade: #86888f; --ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab; --ion-color-medium-tint: #a2a4ab;
/** light **/
--ion-color-light: #f4f5f8; --ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 244, 244; --ion-color-light-rgb: 244, 244, 244;
--ion-color-light-contrast: #000000; --ion-color-light-contrast: #000000;
@@ -92,229 +75,3 @@
--ion-color-light-shade: #d7d8da; --ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9; --ion-color-light-tint: #f5f6f9;
} }
/* Additional Ionic Colors
// --------------------------------------------------
// In order to add colors to be used with Ionic components,
// the color should be added as a class with the convention `.ion-color-{COLOR}`
// where `{COLOR}` is the color to be used on the Ionic component
// and each variant is defined for the color. For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
*/
.ion-color-favorite {
--ion-color-base: #69bb7b;
--ion-color-base-rgb: 105, 187, 123;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #5ca56c;
--ion-color-tint: #78c288;
}
.ion-color-twitter {
--ion-color-base: #1da1f4;
--ion-color-base-rgb: 29, 161, 244;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1a8ed7;
--ion-color-tint: #34aaf5;
}
.ion-color-google {
--ion-color-base: #dc4a38;
--ion-color-base-rgb: 220, 74, 56;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #c24131;
--ion-color-tint: #e05c4c;
}
.ion-color-vimeo {
--ion-color-base: #23b6ea;
--ion-color-base-rgb: 35, 182, 234;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1fa0ce;
--ion-color-tint: #39bdec;
}
.ion-color-facebook {
--ion-color-base: #3b5998;
--ion-color-base-rgb: 59, 89, 152;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #344e86;
--ion-color-tint: #4f6aa2;
}
/* Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the CSS variables found in Ionic's source files.
// To view all the possible Ionic variables, see:
// https://beta.ionicframework.com/docs/theming/css-variables#ionic-variables
*/
:root {
--ion-headings-font-weight: 300;
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
}
/*
* Dark Theme
* ----------------------------------------------------------------------------
*/
.dark-theme {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66,140,255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* ----------------------------------------------------------------------------
*/
.dark-theme.ios {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-toolbar-background: #0d0d0d;
--ion-item-background: #1c1c1c;
--ion-item-background-activated: #313131;
}
/*
* Material Design Dark Theme
* ----------------------------------------------------------------------------
*/
.dark-theme.md {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
}

View File

@@ -1,15 +0,0 @@
export interface DispatchObject {
[key: string]: any;
type: string;
}
type PromiseResolveValue<T> = T extends Promise<infer R> ? R : T;
type EffectType<T extends (...args: any) => any> = ReturnType<ReturnType<T>>;
type EffectReturnValue<T extends (...args: any) => any> = PromiseResolveValue<
EffectType<T>
>;
export type ActionType<T extends (...args: any) => any> = ReturnType<
T
> extends DispatchObject
? ReturnType<T>
: EffectReturnValue<T>;

View File

@@ -13,7 +13,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve" "jsx": "react"
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -1,3 +0,0 @@
{
"extends": ["tslint-react"]
}

View File

@@ -11,12 +11,12 @@ var Hello = {
{ {
onclick: function() { onclick: function() {
count++; count++;
}, }
}, },
count + ' Clicks' count + ' Clicks'
), )
]); ]);
}, }
}; };
var Splash = { var Splash = {
@@ -24,14 +24,14 @@ var Splash = {
return m( return m(
'a', 'a',
{ {
href: '#!/hello', href: '#!/hello'
}, },
'Enter!' 'Enter!'
); );
}, }
}; };
m.route(root, '/splash', { m.route(root, '/splash', {
'/splash': Splash, '/splash': Splash,
'/hello': Hello, '/hello': Hello
}); });

View File

@@ -1,4 +1,4 @@
![Next.js Logo](../packages/frameworks/logos/next.svg) ![Next.js Logo](../../packages/frameworks/logos/next.svg)
# Next.js Example # Next.js Example

View File

@@ -1,4 +1,4 @@
![Polymer Logo](../packages/frameworks/logos/polymer.svg) ![Polymer Logo](../../packages/frameworks/logos/polymer.svg)
# Polymer Example # Polymer Example

View File

@@ -1,4 +1,4 @@
![Preact Logo](../packages/frameworks/logos/preact.svg) ![Preact Logo](../../packages/frameworks/logos/preact.svg)
# Preact Example # Preact Example

View File

@@ -3,5 +3,5 @@ import { component } from 'riot';
import Random from './random.riot'; import Random from './random.riot';
component(Random)(document.getElementById('app'), { component(Random)(document.getElementById('app'), {
title: 'Hi there!', title: 'Hi there!'
}); });

View File

@@ -6,7 +6,7 @@ module.exports = {
output: { output: {
path: path.resolve(__dirname, 'public'), path: path.resolve(__dirname, 'public'),
publicPath: '/public/', publicPath: '/public/',
filename: 'bundle.js', filename: 'bundle.js'
}, },
devtool: 'inline', devtool: 'inline',
module: { module: {
@@ -18,10 +18,10 @@ module.exports = {
{ {
loader: '@riotjs/webpack-loader', loader: '@riotjs/webpack-loader',
options: { options: {
hot: true, hot: true
}, }
}, }
], ]
}, },
{ {
test: /\.js$/, test: /\.js$/,
@@ -29,10 +29,10 @@ module.exports = {
use: { use: {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: ['@babel/preset-env'], presets: ['@babel/preset-env']
}, }
}, }
}, }
], ]
}, }
}; };

View File

@@ -1,4 +1,4 @@
![Saber Logo](../packages/frameworks/logos/saber.svg) ![Saber Logo](../../packages/frameworks/logos/saber.svg)
# Saber Example # Saber Example

View File

@@ -1,4 +1,4 @@
![UmiJS Logo](../packages/frameworks/logos/umi.svg) ![UmiJS Logo](../../packages/frameworks/logos/umi.svg)
# UmiJS Example # UmiJS Example

View File

@@ -1,4 +1,4 @@
![Vue.js Logo](../packages/frameworks/logos/vue.svg) ![Vue.js Logo](../../packages/frameworks/logos/vue.svg)
# Vue.js Example # Vue.js Example

View File

@@ -1,3 +1,5 @@
module.exports = { module.exports = {
presets: ['@vue/app'], presets: [
}; '@vue/app'
]
}

View File

@@ -21,6 +21,20 @@
"eslint-plugin-vue": "^5.0.0", "eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
}, },
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": { "postcss": {
"plugins": { "plugins": {
"autoprefixer": {} "autoprefixer": {}

View File

@@ -1,8 +1,8 @@
import Vue from 'vue'; import Vue from 'vue'
import App from './App.vue'; import App from './App.vue'
Vue.config.productionTip = false; Vue.config.productionTip = false
new Vue({ new Vue({
render: h => h(App), render: h => h(App),
}).$mount('#app'); }).$mount('#app')

View File

@@ -1,14 +0,0 @@
{
"name": "my_site",
"version": "1.0.0",
"private": true,
"description": "A Zola project, ready for deployment with ZEIT Now.",
"main": "index.js",
"scripts": {
"build": "zola build",
"dev": "zola serve --port $PORT"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -43,7 +43,7 @@
"lint": "eslint . --ext .ts,.js" "lint": "eslint . --ext .ts,.js"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts}": [ "./{*,{api,packages,test,utils}/**/*}.{js,ts}": [
"prettier --write", "prettier --write",
"eslint", "eslint",
"git add" "git add"

View File

@@ -10,7 +10,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"next\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"next\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -38,7 +38,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"gatsby\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"gatsby\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -65,7 +65,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"hexo\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"hexo\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -92,7 +92,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@11ty\\/eleventy\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@11ty\\/eleventy\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -119,11 +119,11 @@
"detectors": { "detectors": {
"some": [ "some": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"docusaurus\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"docusaurus\":\\s*\".+?\"[^}]*}"
}, },
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@docusaurus\\/core\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@docusaurus\\/core\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -151,7 +151,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"preact-cli\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"preact-cli\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -178,7 +178,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"ember-cli\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"ember-cli\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -206,7 +206,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@vue\\/cli-service\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@vue\\/cli-service\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -232,7 +232,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@scullyio\\/init\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@scullyio\\/init\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -260,7 +260,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@angular\\/cli\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@angular\\/cli\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -287,7 +287,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"polymer-cli\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"polymer-cli\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -315,7 +315,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"sirv-cli\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"sirv-cli\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -332,6 +332,34 @@
} }
} }
}, },
{
"name": "Ionic React",
"slug": "ionic-react",
"demo": "https://ionic-react.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/zeit/now/master/packages/frameworks/logos/ionic-react.svg",
"tagline": "Ionic React allows you to build mobile PWAs with React and the Ionic Framework.",
"description": "An Ionic React site, created with the Ionic CLI.",
"website": "https://ionicframework.com",
"detectors": {
"every": [
{
"path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@ionic\\/react\":\\s*\".+?\"[^}]*}"
}
]
},
"settings": {
"buildCommand": {
"placeholder": "`build` from `package.json`"
},
"devCommand": {
"value": "stencil build --dev --watch --serve --port $PORT"
},
"outputDirectory": {
"value": "public"
}
}
},
{ {
"name": "Create React App", "name": "Create React App",
"slug": "create-react-app", "slug": "create-react-app",
@@ -343,11 +371,11 @@
"detectors": { "detectors": {
"some": [ "some": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"react-scripts\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"react-scripts\":\\s*\".+?\"[^}]*}"
}, },
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"react-dev-utils\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"react-dev-utils\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -374,7 +402,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"gridsome\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"gridsome\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -402,7 +430,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"umi\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"umi\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -430,7 +458,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"sapper\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"sapper\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -457,7 +485,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"saber\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"saber\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -484,7 +512,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@stencil\\/core\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@stencil\\/core\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -512,7 +540,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "package.json", "path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"nuxt\":\\s*\".+?\"[^}]*}" "matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"nuxt\":\\s*\".+?\"[^}]*}"
} }
] ]
@@ -540,13 +568,13 @@
"detectors": { "detectors": {
"some": [ "some": [
{ {
"file": "config.yaml" "path": "config.yaml"
}, },
{ {
"file": "config.toml" "path": "config.toml"
}, },
{ {
"file": "config.json" "path": "config.json"
} }
] ]
}, },
@@ -572,7 +600,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "_config.yml" "path": "_config.yml"
} }
] ]
}, },
@@ -598,7 +626,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "brunch-config.js" "path": "brunch-config.js"
} }
] ]
}, },
@@ -624,7 +652,7 @@
"detectors": { "detectors": {
"every": [ "every": [
{ {
"file": "config.rb" "path": "config.rb"
} }
] ]
}, },
@@ -773,15 +801,6 @@
"tagline": "A static site generator for Grunt.js and Yeoman, Assemble makes it dead simple to build modular sites and blogs.", "tagline": "A static site generator for Grunt.js and Yeoman, Assemble makes it dead simple to build modular sites and blogs.",
"description": "An Assemble site, created from the Assemble quickstart." "description": "An Assemble site, created from the Assemble quickstart."
}, },
{
"name": "Ionic React",
"slug": "ionic-react",
"demo": "https://ionic-react.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/zeit/now/master/packages/frameworks/logos/ionic-react.svg",
"tagline": "Ionic React allows you to build mobile PWAs with React and the Ionic Framework.",
"description": "An Ionic React site, created with the Ionic CLI.",
"website": "https://ionicframework.com"
},
{ {
"name": "Foundation", "name": "Foundation",
"slug": "foundation", "slug": "foundation",

View File

@@ -1,5 +1,5 @@
export interface FrameworkDetectionItem { export interface FrameworkDetectionItem {
file: string; path: string;
matchContent?: string; matchContent?: string;
} }

View File

@@ -23,18 +23,22 @@ async function matches(fs: DetectorFilesystem, framework: Framework) {
return false; return false;
} }
const check = async ({ file, matchContent }: FrameworkDetectionItem) => { const check = async ({ path, matchContent }: FrameworkDetectionItem) => {
if (!file) { if (!path) {
return false; return false;
} }
if ((await fs.exists(file)) === false) { if ((await fs.hasPath(path)) === false) {
return false; return false;
} }
if (matchContent) { if (matchContent) {
if ((await fs.isFile(path)) === false) {
return false;
}
const regex = new RegExp(matchContent, 'gm'); const regex = new RegExp(matchContent, 'gm');
const content = await fs.readFile(file); const content = await fs.readFile(path);
if (!regex.test(content.toString())) { if (!regex.test(content.toString())) {
return false; return false;

View File

@@ -24,22 +24,34 @@
* methods in this class definition. * methods in this class definition.
*/ */
export abstract class DetectorFilesystem { export abstract class DetectorFilesystem {
protected abstract _hasPath(name: string): Promise<boolean>;
protected abstract _readFile(name: string): Promise<Buffer>; protected abstract _readFile(name: string): Promise<Buffer>;
protected abstract _exists(name: string): Promise<boolean>; protected abstract _isFile(name: string): Promise<boolean>;
private existsCache: Map<string, Promise<boolean>>; private pathCache: Map<string, Promise<boolean>>;
private fileCache: Map<string, Promise<boolean>>;
private readFileCache: Map<string, Promise<Buffer>>; private readFileCache: Map<string, Promise<Buffer>>;
constructor() { constructor() {
this.existsCache = new Map(); this.pathCache = new Map();
this.fileCache = new Map();
this.readFileCache = new Map(); this.readFileCache = new Map();
} }
public exists = async (name: string): Promise<boolean> => { public hasPath = async (path: string): Promise<boolean> => {
let p = this.existsCache.get(name); let p = this.pathCache.get(path);
if (!p) { if (!p) {
p = this._exists(name); p = this._hasPath(path);
this.existsCache.set(name, p); this.pathCache.set(path, p);
}
return p;
};
public isFile = async (name: string): Promise<boolean> => {
let p = this.fileCache.get(name);
if (!p) {
p = this._isFile(name);
this.fileCache.set(name, p);
} }
return p; return p;
}; };

View File

@@ -21,7 +21,17 @@ class VirtualFilesystem extends DetectorFilesystem {
}); });
} }
async _exists(name: string): Promise<boolean> { async _hasPath(path: string): Promise<boolean> {
for (const file of this.files.keys()) {
if (file.startsWith(path)) {
return true;
}
}
return false;
}
async _isFile(name: string): Promise<boolean> {
return this.files.has(name); return this.files.has(name);
} }
@@ -89,6 +99,7 @@ describe('#detectFramework', () => {
it('Detect Hugo #1', async () => { it('Detect Hugo #1', async () => {
const fs = new VirtualFilesystem({ const fs = new VirtualFilesystem({
'config.yaml': 'config', 'config.yaml': 'config',
'content/post.md': '# hello world',
}); });
expect(await detectFramework({ fs, frameworkList })).toBe('hugo'); expect(await detectFramework({ fs, frameworkList })).toBe('hugo');
@@ -97,6 +108,7 @@ describe('#detectFramework', () => {
it('Detect Hugo #2', async () => { it('Detect Hugo #2', async () => {
const fs = new VirtualFilesystem({ const fs = new VirtualFilesystem({
'config.json': 'config', 'config.json': 'config',
'content/post.md': '# hello world',
}); });
expect(await detectFramework({ fs, frameworkList })).toBe('hugo'); expect(await detectFramework({ fs, frameworkList })).toBe('hugo');
@@ -105,6 +117,7 @@ describe('#detectFramework', () => {
it('Detect Hugo #3', async () => { it('Detect Hugo #3', async () => {
const fs = new VirtualFilesystem({ const fs = new VirtualFilesystem({
'config.toml': 'config', 'config.toml': 'config',
'content/post.md': '# hello world',
}); });
expect(await detectFramework({ fs, frameworkList })).toBe('hugo'); expect(await detectFramework({ fs, frameworkList })).toBe('hugo');

View File

@@ -3,6 +3,10 @@ import { promisify } from 'util';
import { join } from 'path'; import { join } from 'path';
import { readConfigFile } from '@now/build-utils'; import { readConfigFile } from '@now/build-utils';
import { Route } from '@now/routing-utils'; import { Route } from '@now/routing-utils';
import NowFrameworks, {
Framework as NowFramework,
SettingValue,
} from '@now/frameworks';
const readirPromise = promisify(readdir); const readirPromise = promisify(readdir);
const readFilePromise = promisify(readFile); const readFilePromise = promisify(readFile);
@@ -11,6 +15,17 @@ const unlinkPromise = promisify(unlink);
const isDir = async (file: string): Promise<boolean> => const isDir = async (file: string): Promise<boolean> =>
(await statPromise(file)).isDirectory(); (await statPromise(file)).isDirectory();
export interface Framework {
name: string;
slug: string;
dependency?: string;
getOutputDirName: (dirPrefix: string) => Promise<string>;
defaultRoutes?: Route[] | ((dirPrefix: string) => Promise<Route[]>);
cachePattern?: string;
buildCommand?: string;
devCommand?: string;
}
// Please note that is extremely important // Please note that is extremely important
// that the `dependency` property needs // that the `dependency` property needs
// to reference a CLI. This is needed because // to reference a CLI. This is needed because
@@ -20,11 +35,12 @@ const isDir = async (file: string): Promise<boolean> =>
// Instead, you need to look for `preact-cli` // Instead, you need to look for `preact-cli`
// when optimizing Preact CLI projects. // when optimizing Preact CLI projects.
export const frameworks: Framework[] = [ const frameworkList: Framework[] = [
{ {
name: 'Gatsby.js', name: 'Gatsby.js',
slug: 'gatsby', slug: 'gatsby',
dependency: 'gatsby', dependency: 'gatsby',
buildCommand: 'gatsby build',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultRoutes: async (dirPrefix: string) => { defaultRoutes: async (dirPrefix: string) => {
try { try {
@@ -52,18 +68,21 @@ export const frameworks: Framework[] = [
name: 'Hexo', name: 'Hexo',
slug: 'hexo', slug: 'hexo',
dependency: 'hexo', dependency: 'hexo',
buildCommand: 'hexo generate',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
}, },
{ {
name: 'Eleventy', name: 'Eleventy',
slug: 'eleventy', slug: 'eleventy',
dependency: '@11ty/eleventy', dependency: '@11ty/eleventy',
buildCommand: 'npx @11ty/eleventy',
getOutputDirName: async () => '_site', getOutputDirName: async () => '_site',
}, },
{ {
name: 'Docusaurus 2.0', name: 'Docusaurus',
slug: 'docusaurus', slug: 'docusaurus',
dependency: '@docusaurus/core', dependency: '@docusaurus/core',
buildCommand: 'docusaurus-build',
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'build'; const base = 'build';
const location = join(dirPrefix, base); const location = join(dirPrefix, base);
@@ -81,6 +100,7 @@ export const frameworks: Framework[] = [
name: 'Preact', name: 'Preact',
slug: 'preact', slug: 'preact',
dependency: 'preact-cli', dependency: 'preact-cli',
buildCommand: 'preact build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -96,6 +116,7 @@ export const frameworks: Framework[] = [
name: 'Ember', name: 'Ember',
slug: 'ember', slug: 'ember',
dependency: 'ember-cli', dependency: 'ember-cli',
buildCommand: 'ember build',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -111,6 +132,7 @@ export const frameworks: Framework[] = [
name: 'Vue.js', name: 'Vue.js',
slug: 'vue', slug: 'vue',
dependency: '@vue/cli-service', dependency: '@vue/cli-service',
buildCommand: 'vue-cli-service build',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -136,7 +158,6 @@ export const frameworks: Framework[] = [
name: 'Scully', name: 'Scully',
slug: 'scully', slug: 'scully',
dependency: '@scullyio/init', dependency: '@scullyio/init',
minNodeRange: '10.x',
buildCommand: 'ng build && scully', buildCommand: 'ng build && scully',
getOutputDirName: async () => 'dist/static', getOutputDirName: async () => 'dist/static',
}, },
@@ -144,7 +165,7 @@ export const frameworks: Framework[] = [
name: 'Angular', name: 'Angular',
slug: 'angular', slug: 'angular',
dependency: '@angular/cli', dependency: '@angular/cli',
minNodeRange: '10.x', buildCommand: 'ng build',
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'dist'; const base = 'dist';
const location = join(dirPrefix, base); const location = join(dirPrefix, base);
@@ -171,6 +192,7 @@ export const frameworks: Framework[] = [
name: 'Polymer', name: 'Polymer',
slug: 'polymer', slug: 'polymer',
dependency: 'polymer-cli', dependency: 'polymer-cli',
buildCommand: 'polymer build',
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'build'; const base = 'build';
const location = join(dirPrefix, base); const location = join(dirPrefix, base);
@@ -193,6 +215,7 @@ export const frameworks: Framework[] = [
name: 'Svelte', name: 'Svelte',
slug: 'svelte', slug: 'svelte',
dependency: 'sirv-cli', dependency: 'sirv-cli',
buildCommand: 'rollup -c',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -208,6 +231,7 @@ export const frameworks: Framework[] = [
name: 'Create React App', name: 'Create React App',
slug: 'create-react-app', slug: 'create-react-app',
dependency: 'react-scripts', dependency: 'react-scripts',
buildCommand: 'react-scripts build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -238,6 +262,7 @@ export const frameworks: Framework[] = [
name: 'Create React App (ejected)', name: 'Create React App (ejected)',
slug: 'create-react-app', slug: 'create-react-app',
dependency: 'react-dev-utils', dependency: 'react-dev-utils',
buildCommand: 'react-scripts build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -268,12 +293,14 @@ export const frameworks: Framework[] = [
name: 'Gridsome', name: 'Gridsome',
slug: 'gridsome', slug: 'gridsome',
dependency: 'gridsome', dependency: 'gridsome',
buildCommand: 'gridsome build',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
}, },
{ {
name: 'UmiJS', name: 'UmiJS',
slug: 'umijs', slug: 'umijs',
dependency: 'umi', dependency: 'umi',
buildCommand: 'umi build',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -289,6 +316,7 @@ export const frameworks: Framework[] = [
name: 'Docusaurus 1.0', name: 'Docusaurus 1.0',
slug: 'docusaurus', slug: 'docusaurus',
dependency: 'docusaurus', dependency: 'docusaurus',
buildCommand: 'docusaurus-build',
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'build'; const base = 'build';
const location = join(dirPrefix, base); const location = join(dirPrefix, base);
@@ -306,12 +334,14 @@ export const frameworks: Framework[] = [
name: 'Sapper', name: 'Sapper',
slug: 'sapper', slug: 'sapper',
dependency: 'sapper', dependency: 'sapper',
buildCommand: 'sapper export',
getOutputDirName: async () => '__sapper__/export', getOutputDirName: async () => '__sapper__/export',
}, },
{ {
name: 'Saber', name: 'Saber',
slug: 'saber', slug: 'saber',
dependency: 'saber', dependency: 'saber',
buildCommand: 'saber build',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -332,6 +362,7 @@ export const frameworks: Framework[] = [
name: 'Stencil', name: 'Stencil',
slug: 'stencil', slug: 'stencil',
dependency: '@stencil/core', dependency: '@stencil/core',
buildCommand: 'stencil build',
getOutputDirName: async () => 'www', getOutputDirName: async () => 'www',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -354,7 +385,6 @@ export const frameworks: Framework[] = [
name: 'Hugo', name: 'Hugo',
slug: 'hugo', slug: 'hugo',
buildCommand: 'hugo -D', buildCommand: 'hugo -D',
devCommand: 'hugo server -D -w -p $PORT',
getOutputDirName: async (dirPrefix: string): Promise<string> => { getOutputDirName: async (dirPrefix: string): Promise<string> => {
const config = await readConfigFile( const config = await readConfigFile(
['config.json', 'config.yaml', 'config.toml'].map(fileName => { ['config.json', 'config.yaml', 'config.toml'].map(fileName => {
@@ -369,7 +399,6 @@ export const frameworks: Framework[] = [
name: 'Jekyll', name: 'Jekyll',
slug: 'jekyll', slug: 'jekyll',
buildCommand: 'jekyll build', buildCommand: 'jekyll build',
devCommand: 'bundle exec jekyll serve --watch --port $PORT',
getOutputDirName: async (dirPrefix: string): Promise<string> => { getOutputDirName: async (dirPrefix: string): Promise<string> => {
const config = await readConfigFile(join(dirPrefix, '_config.yml')); const config = await readConfigFile(join(dirPrefix, '_config.yml'));
return (config && config.destination) || '_site'; return (config && config.destination) || '_site';
@@ -378,25 +407,48 @@ export const frameworks: Framework[] = [
{ {
name: 'Brunch', name: 'Brunch',
slug: 'brunch', slug: 'brunch',
buildCommand: 'brunch build --production',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
}, },
{ {
name: 'Middleman', name: 'Middleman',
slug: 'middleman', slug: 'middleman',
buildCommand: 'bundle exec middleman build', buildCommand: 'bundle exec middleman build',
devCommand: 'bundle exec middleman server -p $PORT',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
}, },
{
name: 'Zola',
slug: 'zola',
buildCommand: 'zola build',
getOutputDirName: async () => 'public',
},
]; ];
export interface Framework { function getValue(
name: string; framework: NowFramework | undefined,
slug: string; name: keyof NowFramework['settings']
dependency?: string; ) {
getOutputDirName: (dirPrefix: string) => Promise<string>; const setting = framework && framework.settings && framework.settings[name];
defaultRoutes?: Route[] | ((dirPrefix: string) => Promise<Route[]>); return setting && (setting as SettingValue).value;
minNodeRange?: string;
cachePattern?: string;
buildCommand?: string;
devCommand?: string;
} }
export const frameworks: Framework[] = frameworkList.map(partialFramework => {
const frameworkItem = (NowFrameworks as NowFramework[]).find(
f => f.slug === partialFramework.slug
);
const devCommand = getValue(frameworkItem, 'devCommand');
const buildCommand = getValue(frameworkItem, 'buildCommand');
const outputDirectory = getValue(frameworkItem, 'outputDirectory');
const getOutputDirName = partialFramework.getOutputDirName
? partialFramework.getOutputDirName
: async () => outputDirectory || 'public';
return {
devCommand,
buildCommand,
...partialFramework,
getOutputDirName,
};
});

View File

@@ -212,21 +212,24 @@ export async function build({
const pkg = getPkg(entrypoint, workPath); const pkg = getPkg(entrypoint, workPath);
const devScript = pkg ? getCommand(pkg, 'dev', config) : null;
const buildScript = pkg ? getCommand(pkg, 'build', config) : null;
const framework = getFramework(config, pkg); const framework = getFramework(config, pkg);
const devCommand: string | undefined = const devCommand: string | undefined =
config.devCommand || (framework && framework.devCommand); config.devCommand ||
(devScript ? undefined : framework && framework.devCommand);
const buildCommand: string | undefined = const buildCommand: string | undefined =
config.buildCommand || (framework && framework.buildCommand); config.buildCommand ||
(buildScript ? undefined : framework && framework.buildCommand);
if (pkg || buildCommand) { if (pkg || buildCommand) {
const gemfilePath = path.join(workPath, 'Gemfile'); const gemfilePath = path.join(workPath, 'Gemfile');
const requirementsPath = path.join(workPath, 'requirements.txt'); const requirementsPath = path.join(workPath, 'requirements.txt');
let output: Files = {}; let output: Files = {};
let minNodeRange: string | undefined = undefined;
const routes: Route[] = []; const routes: Route[] = [];
const devScript = pkg ? getCommand(pkg, 'dev', config) : null;
if (config.zeroConfig) { if (config.zeroConfig) {
if (existsSync(gemfilePath) && !meta.isDev) { if (existsSync(gemfilePath) && !meta.isDev) {
@@ -284,22 +287,11 @@ export async function build({
debug( debug(
`Detected ${framework.name} framework. Optimizing your deployment...` `Detected ${framework.name} framework. Optimizing your deployment...`
); );
if (framework.minNodeRange) {
minNodeRange = framework.minNodeRange;
debug(
`${framework.name} requires Node.js ${framework.minNodeRange}. Switching...`
);
} else {
debug(
`${framework.name} does not require a specific Node.js version. Continuing ...`
);
}
} }
const nodeVersion = await getNodeVersion( const nodeVersion = await getNodeVersion(
entrypointDir, entrypointDir,
minNodeRange, undefined,
config, config,
meta meta
); );
@@ -394,7 +386,6 @@ export async function build({
); );
} }
const buildScript = pkg ? getCommand(pkg, 'build', config) : null;
debug( debug(
`Running "${buildCommand || buildScript}" script in "${entrypoint}"` `Running "${buildCommand || buildScript}" script in "${entrypoint}"`
); );

View File

@@ -0,0 +1,3 @@
public
.env
processed_images/

View File

@@ -0,0 +1,14 @@
base_url = "/"
compile_sass = false
theme = "feather"
highlight_code = true
build_search_index = false
title = "Hello World"
description = "A an example zola website with a theme"
default_language = "en"
generate_categories_pages = true
generate_tags_pages = true
generate_rss = true
[extra]
#feather_header_image = "/example.svg"

View File

@@ -0,0 +1,17 @@
+++
title = "Hello Universe"
weight = 1
[extra]
number = "1"
+++
## This is the first.
Hello to anyone out there. Thanks for coming to my Ted Talk. Goodnight!
<!-- more -->
## Keywords
_default, first, hello_

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,19 @@
+++
title = "Never Gonna Give You Up"
weight = 2
[extra]
number = "2"
+++
{{youtube(id="dQw4w9WgXcQ") }}
## Let's talk about youtube
Everyone loves youtube because of the endless videos. I could never give up youtube, could you?
<!-- more -->
## Keywords
_youtube, video, never_

Some files were not shown because too many files have changed in this diff Show More