├── isucari └── webapp │ ├── python │ ├── .gitignore │ ├── templates │ └── requirements.txt │ ├── nodejs │ ├── public │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── package.json │ └── api.ts │ ├── perl │ ├── public │ ├── .gitignore │ ├── lib │ │ └── Isucari.pm │ ├── t │ │ └── 00_compile.t │ ├── Makefile.PL │ ├── cpanfile │ └── app.psgi │ ├── ruby │ ├── public │ ├── lib │ │ ├── isucari.rb │ │ └── isucari │ │ │ └── api.rb │ ├── .gitignore │ ├── config.ru │ ├── Gemfile │ └── Gemfile.lock │ ├── go │ ├── .gitignore │ ├── go.mod │ ├── mysql.go │ └── categories.go │ ├── sql │ ├── .gitignore │ ├── 00_create_database.sql │ ├── init.sh │ └── 02_categories.sql │ ├── php │ ├── .gitattributes │ ├── .gitignore │ ├── src │ │ ├── App │ │ │ └── Environment.php │ │ ├── middleware.php │ │ └── settings.php │ ├── composer.json │ └── public │ │ └── index.php │ ├── public │ ├── .gitignore │ ├── logo.png │ ├── favicon.png │ ├── logo.png.gz │ ├── index.html.gz │ ├── not_found.png │ ├── favicon.png.gz │ ├── manifest.json.gz │ ├── not_found.png.gz │ ├── service-worker.js.gz │ ├── asset-manifest.json.gz │ ├── internal_server_error.png │ ├── internal_server_error.png.gz │ ├── static │ │ ├── js │ │ │ ├── 2.ff6e1067.chunk.js.gz │ │ │ ├── 2.ff6e1067.chunk.js.map.gz │ │ │ ├── main.babc3d4d.chunk.js.gz │ │ │ ├── runtime~main.a8a9905a.js.gz │ │ │ ├── main.babc3d4d.chunk.js.map.gz │ │ │ ├── runtime~main.a8a9905a.js.map.gz │ │ │ └── runtime~main.a8a9905a.js │ │ └── css │ │ │ ├── main.19393e92.chunk.css.gz │ │ │ ├── main.19393e92.chunk.css.map.gz │ │ │ ├── main.19393e92.chunk.css │ │ │ └── main.19393e92.chunk.css.map │ ├── precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js.gz │ ├── manifest.json │ ├── precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js │ ├── asset-manifest.json │ ├── service-worker.js │ └── index.html │ ├── frontend │ ├── src │ │ ├── config.ts │ │ ├── components │ │ │ ├── Item │ │ │ │ ├── index.tsx │ │ │ │ ├── Item.stories.tsx │ │ │ │ └── Item.tsx │ │ │ ├── Header │ │ │ │ ├── index.tsx │ │ │ │ └── Header.stories.tsx │ │ │ ├── ItemFooter │ │ │ │ ├── index.tsx │ │ │ │ ├── ItemFooter.stories.tsx │ │ │ │ └── ItemFooter.tsx │ │ │ ├── ItemImage │ │ │ │ ├── index.tsx │ │ │ │ ├── ItemImage.stories.tsx │ │ │ │ └── ItemImage.tsx │ │ │ ├── ItemList │ │ │ │ ├── index.tsx │ │ │ │ ├── ItemList.stories.tsx │ │ │ │ └── ItemList.tsx │ │ │ ├── SnackBar │ │ │ │ ├── index.tsx │ │ │ │ ├── SnackBar.stories.tsx │ │ │ │ └── SnackBar.tsx │ │ │ ├── LoadingButton │ │ │ │ ├── index.tsx │ │ │ │ ├── LoadingButton.stories.tsx │ │ │ │ └── LoadingButton.tsx │ │ │ ├── TimelineLoading │ │ │ │ ├── index.tsx │ │ │ │ ├── TimelineLoading.stories.tsx │ │ │ │ └── TimelineLoading.tsx │ │ │ ├── TransactionList │ │ │ │ ├── index.tsx │ │ │ │ ├── TransactionList.stories.tsx │ │ │ │ └── TransactionList.tsx │ │ │ ├── TransactionBuyer │ │ │ │ ├── index.tsx │ │ │ │ ├── TransactionBuyer.stories.tsx │ │ │ │ └── TransactionBuyer.tsx │ │ │ ├── TransactionLabel │ │ │ │ ├── index.tsx │ │ │ │ ├── TransactionLabel.stories.tsx │ │ │ │ └── TransactionLabel.tsx │ │ │ ├── TransactionSeller │ │ │ │ ├── index.tsx │ │ │ │ ├── TransactionSeller.stories.tsx │ │ │ │ └── TransactionSeller.tsx │ │ │ ├── TransactionComponent │ │ │ │ ├── index.tsx │ │ │ │ ├── TransactionComponent.stories.tsx │ │ │ │ └── TransactionComponent.tsx │ │ │ ├── Transaction │ │ │ │ ├── Buyer │ │ │ │ │ ├── Done.tsx │ │ │ │ │ ├── Initial.tsx │ │ │ │ │ ├── WaitShipping.tsx │ │ │ │ │ └── WaitDone.tsx │ │ │ │ └── Seller │ │ │ │ │ ├── Done.tsx │ │ │ │ │ ├── WaitDone.tsx │ │ │ │ │ ├── Initial.tsx │ │ │ │ │ └── WaitShipping.tsx │ │ │ ├── ErrorMessageComponent.tsx │ │ │ ├── SellingButtonComponent.tsx │ │ │ ├── LoadingComponent.tsx │ │ │ ├── Route │ │ │ │ ├── AuthRoute.tsx │ │ │ │ └── NonAuthRoute.tsx │ │ │ └── BasePageComponent.tsx │ │ ├── react-app-env.d.ts │ │ ├── pages │ │ │ ├── error │ │ │ │ ├── NotFoundPage │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── NotFoundPage.stories.tsx │ │ │ │ │ └── NotFoundPage.tsx │ │ │ │ └── InternalServerErrorPage │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── InternalServerErrorPage.stories.tsx │ │ │ │ │ └── InternalServerErrorPage.tsx │ │ │ ├── BuyComplete.tsx │ │ │ ├── SignInPage.tsx │ │ │ ├── SignUpPage.tsx │ │ │ ├── SellPage.tsx │ │ │ ├── ItemBuyPage.tsx │ │ │ ├── ItemListPage.tsx │ │ │ ├── TopPage.tsx │ │ │ └── UserSettingPage.tsx │ │ ├── dataObjects │ │ │ ├── shipping.ts │ │ │ ├── transaction.ts │ │ │ ├── user.ts │ │ │ ├── settings.ts │ │ │ ├── category.ts │ │ │ └── item.ts │ │ ├── errors │ │ │ ├── NotFoundError.ts │ │ │ ├── InternalServerError.ts │ │ │ ├── AppResponseError.ts │ │ │ ├── PaymentResponseError.ts │ │ │ └── ResponseError.ts │ │ ├── middlewares │ │ │ ├── index.ts │ │ │ └── checkLocationChange.ts │ │ ├── types │ │ │ └── paymentApiTypes.ts │ │ ├── App.tsx │ │ ├── App.test.tsx │ │ ├── actions │ │ │ ├── snackBarAction.ts │ │ │ ├── locationChangeAction.ts │ │ │ ├── errorAction.ts │ │ │ ├── actionTypes.ts │ │ │ ├── postBumpAction.ts │ │ │ ├── authenticationActions.ts │ │ │ ├── registerAction.ts │ │ │ ├── postShippedAction.ts │ │ │ ├── sellingItemAction.ts │ │ │ ├── postCompleteAction.ts │ │ │ └── postShippedDoneAction.ts │ │ ├── index.css │ │ ├── theme.ts │ │ ├── containers │ │ │ ├── NotFoundContainer.tsx │ │ │ ├── InternalServerContainer.tsx │ │ │ ├── BasePageContainer.tsx │ │ │ ├── UserSettingPageContainer.tsx │ │ │ ├── BuyCompleteContainer.tsx │ │ │ ├── ItemBuyPageContainer.tsx │ │ │ ├── BuyerTransactionContainer.tsx │ │ │ ├── SnackBarContainer.tsx │ │ │ ├── SignInFormContainer.tsx │ │ │ ├── SignUpFormContainer.tsx │ │ │ ├── SellingButtonContainer.tsx │ │ │ ├── AuthContainer.tsx │ │ │ ├── NonAuthContainer.tsx │ │ │ ├── ItemListPageContainer.tsx │ │ │ ├── TransactionContainer.tsx │ │ │ ├── ItemBuyFormContainer.tsx │ │ │ ├── TransactionPageContainer.tsx │ │ │ ├── SellerTransactionContainer.tsx │ │ │ ├── ItemEditPageContainer.tsx │ │ │ ├── CategoryItemListPageContainer.tsx │ │ │ ├── SellFormContainer.tsx │ │ │ ├── ItemPageContainer.tsx │ │ │ ├── HeaderContainer.tsx │ │ │ └── UserPageContainer.tsx │ │ ├── configureStore.ts │ │ ├── App.css │ │ ├── reducers │ │ │ ├── viewingItemReducer.ts │ │ │ ├── categoriesReducer.ts │ │ │ ├── buyPageReducer.ts │ │ │ ├── viewingUserReducer.ts │ │ │ ├── index.ts │ │ │ ├── timelineReducer.ts │ │ │ ├── userItemsReducer.ts │ │ │ ├── formErrorReducer.ts │ │ │ ├── transactionsReducer.ts │ │ │ ├── snackBarReducer.ts │ │ │ ├── errorReducer.ts │ │ │ ├── authStatusReducer.ts │ │ │ └── pageReducuer.ts │ │ ├── actionHelper │ │ │ ├── ajaxErrorHandler.ts │ │ │ └── responseChecker.ts │ │ ├── index.tsx │ │ ├── httpClients │ │ │ ├── paymentClient.ts │ │ │ └── appClient.ts │ │ └── hoc │ │ │ └── withBaseComponent.tsx │ ├── .storybook │ │ ├── addons.js │ │ ├── webpack.config.js │ │ └── config.tsx │ ├── public │ │ ├── logo.png │ │ ├── favicon.png │ │ ├── not_found.png │ │ ├── internal_server_error.png │ │ ├── manifest.json │ │ └── index.html │ ├── tool │ │ └── clean.js │ ├── .prettierrc.js │ ├── .gitignore │ ├── README.md │ ├── tsconfig.json │ └── package.json │ ├── docs │ ├── images │ │ ├── 1-1.png │ │ ├── 2-1.png │ │ ├── 2-2.png │ │ ├── 3-1.png │ │ ├── 3-2.png │ │ └── logo.png │ └── APPLICATION_SPEC.md │ └── README.md ├── .config └── configstore │ └── update-notifier-npm.json ├── env.sh ├── alp.yml ├── isu01 ├── isucari.golang.service └── nginx.conf ├── .bash_profile ├── .profile ├── .gitignore ├── recipe.rb ├── isu02 └── nginx.conf └── isu03 └── nginx.conf /isucari/webapp/python/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /isucari/webapp/nodejs/public: -------------------------------------------------------------------------------- 1 | ../public -------------------------------------------------------------------------------- /isucari/webapp/perl/public: -------------------------------------------------------------------------------- 1 | ../public -------------------------------------------------------------------------------- /isucari/webapp/ruby/public: -------------------------------------------------------------------------------- 1 | ../public -------------------------------------------------------------------------------- /isucari/webapp/go/.gitignore: -------------------------------------------------------------------------------- 1 | isucari 2 | -------------------------------------------------------------------------------- /isucari/webapp/perl/.gitignore: -------------------------------------------------------------------------------- 1 | /local 2 | -------------------------------------------------------------------------------- /isucari/webapp/python/templates: -------------------------------------------------------------------------------- 1 | ../public -------------------------------------------------------------------------------- /isucari/webapp/sql/.gitignore: -------------------------------------------------------------------------------- 1 | initial.sql 2 | -------------------------------------------------------------------------------- /isucari/webapp/nodejs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.js 3 | 4 | -------------------------------------------------------------------------------- /isucari/webapp/ruby/lib/isucari.rb: -------------------------------------------------------------------------------- 1 | module Isucari 2 | end 3 | -------------------------------------------------------------------------------- /isucari/webapp/php/.gitattributes: -------------------------------------------------------------------------------- 1 | composer.lock diff=nodiff 2 | -------------------------------------------------------------------------------- /isucari/webapp/public/.gitignore: -------------------------------------------------------------------------------- 1 | upload/ 2 | initial.zip 3 | 4 | -------------------------------------------------------------------------------- /isucari/webapp/ruby/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /vendor/bundle/ 3 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/config.ts: -------------------------------------------------------------------------------- 1 | export const shopID = '11'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/php/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | logs/* 3 | .php_cs.cache 4 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Item/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Item'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemFooter/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ItemFooter'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemImage/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ItemImage'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemList/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ItemList'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/SnackBar/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SnackBar'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/error/NotFoundPage/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './NotFoundPage'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/LoadingButton/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './LoadingButton'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TimelineLoading/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './TimelineLoading'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionList/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './TransactionList'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionBuyer/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './TransactionBuyer'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionLabel/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './TransactionLabel'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionSeller/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './TransactionSeller'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/nodejs/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /.config/configstore/update-notifier-npm.json: -------------------------------------------------------------------------------- 1 | { 2 | "optOut": false, 3 | "lastUpdateCheck": 1567762362644 4 | } -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionComponent/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './TransactionComponent'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/error/InternalServerErrorPage/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './InternalServerErrorPage'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/logo.png -------------------------------------------------------------------------------- /isucari/webapp/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/favicon.png -------------------------------------------------------------------------------- /isucari/webapp/public/logo.png.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/logo.png.gz -------------------------------------------------------------------------------- /isucari/webapp/docs/images/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/docs/images/1-1.png -------------------------------------------------------------------------------- /isucari/webapp/docs/images/2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/docs/images/2-1.png -------------------------------------------------------------------------------- /isucari/webapp/docs/images/2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/docs/images/2-2.png -------------------------------------------------------------------------------- /isucari/webapp/docs/images/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/docs/images/3-1.png -------------------------------------------------------------------------------- /isucari/webapp/docs/images/3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/docs/images/3-2.png -------------------------------------------------------------------------------- /isucari/webapp/docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/docs/images/logo.png -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/dataObjects/shipping.ts: -------------------------------------------------------------------------------- 1 | export type ShippingStatus = 'initial' | 'wait_pickup' | 'shipping' | 'done'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/dataObjects/transaction.ts: -------------------------------------------------------------------------------- 1 | export type TransactionStatus = 'wait_shipping' | 'wait_done' | 'done'; 2 | -------------------------------------------------------------------------------- /isucari/webapp/public/index.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/index.html.gz -------------------------------------------------------------------------------- /isucari/webapp/public/not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/not_found.png -------------------------------------------------------------------------------- /isucari/webapp/frontend/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /isucari/webapp/public/favicon.png.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/favicon.png.gz -------------------------------------------------------------------------------- /isucari/webapp/public/manifest.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/manifest.json.gz -------------------------------------------------------------------------------- /isucari/webapp/public/not_found.png.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/not_found.png.gz -------------------------------------------------------------------------------- /isucari/webapp/frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/frontend/public/logo.png -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/errors/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | // will be handled as HTTP 404 NotFound 2 | export class NotFoundError extends Error {} 3 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/frontend/public/favicon.png -------------------------------------------------------------------------------- /isucari/webapp/public/service-worker.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/service-worker.js.gz -------------------------------------------------------------------------------- /isucari/webapp/frontend/public/not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/frontend/public/not_found.png -------------------------------------------------------------------------------- /isucari/webapp/public/asset-manifest.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/asset-manifest.json.gz -------------------------------------------------------------------------------- /isucari/webapp/public/internal_server_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/internal_server_error.png -------------------------------------------------------------------------------- /isucari/webapp/public/internal_server_error.png.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/internal_server_error.png.gz -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/errors/InternalServerError.ts: -------------------------------------------------------------------------------- 1 | // will be handled as HTTP 500 Internal Server Error 2 | export class InternalServerError extends Error {} 3 | -------------------------------------------------------------------------------- /isucari/webapp/perl/lib/Isucari.pm: -------------------------------------------------------------------------------- 1 | package Isucari; 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | our $VERSION = 0.40; 8 | 9 | 1; 10 | 11 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/errors/AppResponseError.ts: -------------------------------------------------------------------------------- 1 | import { ResponseError } from './ResponseError'; 2 | 3 | export class AppResponseError extends ResponseError {} 4 | -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/2.ff6e1067.chunk.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/js/2.ff6e1067.chunk.js.gz -------------------------------------------------------------------------------- /isucari/webapp/frontend/public/internal_server_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/frontend/public/internal_server_error.png -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/2.ff6e1067.chunk.js.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/js/2.ff6e1067.chunk.js.map.gz -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/main.babc3d4d.chunk.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/js/main.babc3d4d.chunk.js.gz -------------------------------------------------------------------------------- /isucari/webapp/ruby/config.ru: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $:.unshift(lib) unless $:.include?(lib) 3 | 4 | require 'isucari/web' 5 | 6 | run Isucari::Web 7 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/dataObjects/user.ts: -------------------------------------------------------------------------------- 1 | export interface UserData { 2 | id: number; 3 | accountName: string; 4 | address?: string; 5 | numSellItems: number; 6 | } 7 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/errors/PaymentResponseError.ts: -------------------------------------------------------------------------------- 1 | import { ResponseError } from './ResponseError'; 2 | 3 | export class PaymentResponseError extends ResponseError {} 4 | -------------------------------------------------------------------------------- /isucari/webapp/public/static/css/main.19393e92.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/css/main.19393e92.chunk.css.gz -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/runtime~main.a8a9905a.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/js/runtime~main.a8a9905a.js.gz -------------------------------------------------------------------------------- /isucari/webapp/public/static/css/main.19393e92.chunk.css.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/css/main.19393e92.chunk.css.map.gz -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/main.babc3d4d.chunk.js.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/js/main.babc3d4d.chunk.js.map.gz -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/runtime~main.a8a9905a.js.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/static/js/runtime~main.a8a9905a.js.map.gz -------------------------------------------------------------------------------- /isucari/webapp/frontend/tool/clean.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const rimraf = require('rimraf'); 3 | 4 | rimraf.sync(`${__dirname}/../../public/**/{*.png,*.json,*.js,*.html,static/**}`); 5 | -------------------------------------------------------------------------------- /isucari/webapp/perl/t/00_compile.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More; 4 | 5 | use_ok $_ for qw( 6 | Isucari 7 | Isucari::Web 8 | ); 9 | 10 | done_testing; 11 | 12 | 13 | -------------------------------------------------------------------------------- /isucari/webapp/public/precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takonomura/isucon9-qualify/HEAD/isucari/webapp/public/precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js.gz -------------------------------------------------------------------------------- /isucari/webapp/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'puma' 4 | gem 'sinatra' 5 | gem 'mysql2' 6 | gem 'mysql2-cs-bind' 7 | gem 'bcrypt' 8 | 9 | group :development do 10 | gem 'sinatra-contrib' 11 | end 12 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import checkLocationChange from './checkLocationChange'; 2 | import { Middleware } from 'redux'; 3 | 4 | let middleware: Middleware[] = [checkLocationChange]; 5 | 6 | export default middleware; 7 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | MYSQL_HOST=172.24.83.45 2 | MYSQL_PORT=3306 3 | MYSQL_USER=isucari 4 | MYSQL_DBNAME=isucari 5 | MYSQL_PASS=isucari 6 | GOOGLE_APPLICATION_CREDENTIALS=/home/isucon/.ssh/stackdriver_trace_agen_credentials.json 7 | GOOGLE_CLOUD_PROJECT=isucon-213703 8 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi : true, 3 | singleQuote : true, 4 | tabWidth : 2, 5 | trailingComma : 'all', 6 | parser : 'typescript', 7 | filepath : './src/**/*.{ts,tsx}', 8 | }; 9 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/dataObjects/settings.ts: -------------------------------------------------------------------------------- 1 | import { UserData } from './user'; 2 | import { CategorySimple } from './category'; 3 | 4 | export interface Settings { 5 | csrfToken: string; 6 | categories: CategorySimple[]; 7 | user?: UserData; 8 | } 9 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/types/paymentApiTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * POST /card 3 | */ 4 | export interface CardReq { 5 | card_number: string; 6 | shop_id: string; 7 | } 8 | 9 | export interface CardRes extends Response { 10 | token: string; 11 | } 12 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { AppRoute } from './routes/Route'; 4 | 5 | const App: React.FC<{}> = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /isucari/webapp/perl/Makefile.PL: -------------------------------------------------------------------------------- 1 | use ExtUtils::MakeMaker; 2 | 3 | WriteMakefile( 4 | NAME => 'Isucari', 5 | VERSION_FROM => 'lib/Isucari.pm', 6 | PREREQ_PM => { 7 | 'Kossy' => '0.40', 8 | }, 9 | MIN_PERL_VERSION => '5.008001' 10 | ); 11 | 12 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/dataObjects/category.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | id: number; 3 | parentId: number; 4 | categoryName: string; 5 | parentCategoryName: string; 6 | } 7 | 8 | export interface CategorySimple { 9 | id: number; 10 | parentId: number; 11 | categoryName: string; 12 | } 13 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TimelineLoading/TimelineLoading.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { TimelineLoading } from '.'; 4 | 5 | const stories = storiesOf('components/TimelineLoading', module); 6 | 7 | stories.add('default', () => ); 8 | -------------------------------------------------------------------------------- /alp.yml: -------------------------------------------------------------------------------- 1 | file: /var/log/nginx/access.log 2 | sort: count 3 | reverse: true 4 | output: count,2xx,4xx,5xx,method,uri,min,p50,p99,max 5 | matching_groups: 6 | - '^/items/[0-9]+\.json$' 7 | - '^/users/[0-9]+\.json$' 8 | - '^/new_items/[0-9]+\.json$' 9 | - '^/transactions/[0-9]+\.png$' 10 | - '^/upload/[0-9a-f]+\.jpg$' 11 | ltsv: {} 12 | json: {} 13 | regexp: {} 14 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/errors/ResponseError.ts: -------------------------------------------------------------------------------- 1 | export class ResponseError extends Error { 2 | private readonly res: Response | undefined; 3 | 4 | constructor(message: string, response?: Response) { 5 | super(message); 6 | this.res = response; 7 | } 8 | 9 | getResponse(): Response | undefined { 10 | return this.res; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /isucari/webapp/php/src/App/Environment.php: -------------------------------------------------------------------------------- 1 | {} 8 | 9 | export const closeSnackBarAction = (): SnackBarClose => ({ 10 | type: SNACK_BAR_CLOSE, 11 | }); 12 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Buyer/Done.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | type Props = {}; 5 | 6 | const Done: React.FC = () => { 7 | return ( 8 | 9 | 取引が完了しました 10 | 11 | ); 12 | }; 13 | 14 | export default Done; 15 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Seller/Done.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | type Props = {}; 5 | 6 | const Done: React.FC = () => { 7 | return ( 8 | 9 | 取引が完了しました 10 | 11 | ); 12 | }; 13 | 14 | export default Done; 15 | -------------------------------------------------------------------------------- /isucari/webapp/python/requirements.txt: -------------------------------------------------------------------------------- 1 | gunicorn==19.9.0 2 | mysqlclient==1.4.4 3 | bcrypt==3.1.7 4 | certifi==2019.6.16 5 | cffi==1.12.3 6 | chardet==3.0.4 7 | Click==7.0 8 | Flask==1.1.1 9 | idna==2.8 10 | itsdangerous==1.1.0 11 | Jinja2==2.10.1 12 | MarkupSafe==1.1.1 13 | pycparser==2.19 14 | PyMySQL==0.9.3 15 | requests==2.22.0 16 | six==1.12.0 17 | urllib3==1.25.3 18 | Werkzeug==0.15.5 19 | -------------------------------------------------------------------------------- /isucari/webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ISUCARI", 3 | "name": "椅子を売買できる安心安全のフリマアプリ♪", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#f44436", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ISUCARI", 3 | "name": "椅子を売買できる安心安全のフリマアプリ♪", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#f44436", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config, mode }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | loader: require.resolve('babel-loader'), 5 | options: { 6 | presets: [['react-app', { flow: false, typescript: true }]], 7 | }, 8 | }); 9 | config.resolve.extensions.push('.ts', '.tsx'); 10 | return config; 11 | }; -------------------------------------------------------------------------------- /isu01/isucari.golang.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description = isucon9 qualifier main application in golang 3 | 4 | [Service] 5 | WorkingDirectory=/home/isucon/isucari/webapp/go/ 6 | EnvironmentFile=/home/isucon/env.sh 7 | 8 | ExecStart = /home/isucon/isucari/webapp/go/isucari 9 | 10 | Restart = always 11 | Type = simple 12 | User = isucon 13 | Group = isucon 14 | 15 | [Install] 16 | WantedBy = multi-user.target 17 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/locationChangeAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | export const PATH_NAME_CHANGE = 'PATH_NAME_CHANGE'; 4 | 5 | export type LocationChangeActions = PathNameChangeAction; 6 | 7 | export interface PathNameChangeAction extends Action {} 8 | 9 | export const pathNameChangeAction = (): PathNameChangeAction => ({ 10 | type: PATH_NAME_CHANGE, 11 | }); 12 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # build 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /isucari/webapp/sql/00_create_database.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS `isucari`; 2 | CREATE DATABASE `isucari`; 3 | 4 | DROP USER IF EXISTS 'isucari'@'localhost'; 5 | CREATE USER 'isucari'@'localhost' IDENTIFIED BY 'isucari'; 6 | GRANT ALL PRIVILEGES ON `isucari`.* TO 'isucari'@'localhost'; 7 | 8 | DROP USER IF EXISTS 'isucari'@'%'; 9 | CREATE USER 'isucari'@'%' IDENTIFIED BY 'isucari'; 10 | GRANT ALL PRIVILEGES ON `isucari`.* TO 'isucari'@'%'; 11 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Buyer/Initial.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | type Props = {}; 5 | 6 | const Initial: React.FC = () => { 7 | return ( 8 | 9 | 商品を購入しました 10 | 商品が発送されるまでお待ち下さい 11 | 12 | ); 13 | }; 14 | 15 | export default Initial; 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Buyer/WaitShipping.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | type Props = {}; 5 | 6 | const WaitShipping: React.FC = () => { 7 | return ( 8 | 9 | 商品を購入しました 10 | 商品が発送されるまでお待ち下さい 11 | 12 | ); 13 | }; 14 | 15 | export default WaitShipping; 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Seller/WaitDone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | type Props = {}; 5 | 6 | const WaitDone: React.FC = () => { 7 | return ( 8 | 9 | 商品が発送されました 10 | 11 | 購入者が商品を受け取るのをお待ち下さい 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default WaitDone; 18 | -------------------------------------------------------------------------------- /isucari/webapp/README.md: -------------------------------------------------------------------------------- 1 | # isucon9-qualify 2 | 3 | ## アプリケーションのディレクトリ構成 4 | 5 | アプリケーションのディレクトリ構成は以下のようになっています。 6 | 7 | ``` 8 | /home/isucon/isucari/webapp/ 9 | ├── docs # アプリケーションおよび外部サービスについてのドキュメント 10 | ├── frontend # フロントエンドのソースコード 11 | ├── go # Go実装 12 | ├── nodejs # Node.js実装 13 | ├── perl # Perl実装 14 | ├── php # PHP実装 15 | ├── python # Python実装 16 | ├── ruby # Ruby実装 17 | ├── public # jsやcss、画像データ等の静的ファイル 18 | └── sql # データベースのスキーマおよび初期化に必要なSQL 19 | ``` 20 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core'; 2 | 3 | const PRIMARY = '#f44436'; 4 | const SECONDARY = '#4fc3f7'; 5 | const SECONDARY_CONTRAST = '#fff'; 6 | 7 | export const themeInstance = createMuiTheme({ 8 | palette: { 9 | background: { 10 | default: '#fff', 11 | }, 12 | primary: { 13 | main: PRIMARY, 14 | }, 15 | secondary: { 16 | main: SECONDARY, 17 | contrastText: SECONDARY_CONTRAST, 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /isucari/webapp/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/isucon/isucon9-qualify/webapp/go 2 | 3 | go 1.12 4 | 5 | require ( 6 | cloud.google.com/go v0.45.1 7 | contrib.go.opencensus.io/exporter/stackdriver v0.12.7 8 | contrib.go.opencensus.io/integrations/ocsql v0.1.4 9 | github.com/go-sql-driver/mysql v1.4.1 10 | github.com/gorilla/sessions v1.2.0 11 | github.com/jmoiron/sqlx v1.2.0 12 | go.opencensus.io v0.22.1 13 | goji.io v2.0.2+incompatible 14 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 15 | ) 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/NotFoundContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import { NotFoundPage } from '../pages/error/NotFoundPage'; 4 | import { Dispatch } from 'redux'; 5 | 6 | const mapStateToProps = (state: AppState) => ({ 7 | message: state.error.errorMessage, 8 | }); 9 | const mapDispatchToProps = (dispatch: Dispatch) => ({}); 10 | 11 | export default connect( 12 | mapStateToProps, 13 | mapDispatchToProps, 14 | )(NotFoundPage); 15 | -------------------------------------------------------------------------------- /isucari/webapp/perl/cpanfile: -------------------------------------------------------------------------------- 1 | requires 'Kossy', '0.40'; 2 | requires 'DBD::mysql', '4.050'; 3 | requires 'DBIx::Sunny', '0.9991'; 4 | requires 'JSON::XS', '4.02'; 5 | requires 'JSON::Types'; 6 | requires 'Plack::Middleware::Session'; 7 | requires 'HTTP::Date'; 8 | requires 'HTTP::Status'; 9 | requires 'Crypt::Eksblowfish::Bcrypt'; 10 | requires 'Crypt::OpenSSL::Random'; 11 | requires 'Digest::SHA'; 12 | requires 'LWP::UserAgent'; 13 | requires 'IO::Socket::SSL'; 14 | requires 'Starlet'; 15 | requires 'LWP::Protocol::https'; 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/README.md: -------------------------------------------------------------------------------- 1 | # ISUCON9 qualify frontend 2 | 3 | ISUCON9予選のフロントエンドソースコードです 4 | 5 | ## init 6 | 7 | ```sh 8 | npm i 9 | ``` 10 | 11 | ## scripts 12 | 13 | ### `npm run deploy` 14 | 15 | [../public](../public)ディレクトリにビルドしたファイルを配置します。 16 | 17 | **`upload`ディレクトリを除く全てのファイルを上書きするので注意してください** 18 | 19 | ### `npm run storybook` 20 | 21 | Storybookを起動します 22 | 23 | # Other 24 | 25 | - ロゴ作成にはHatchfulを利用しました 26 | - https://hatchful.shopify.com/ 27 | - UIライブラリにMaterial-UIを利用しました 28 | - https://material-ui.com/ 29 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ErrorMessageComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormHelperText } from '@material-ui/core'; 3 | 4 | interface ErrorMessageComponentProps { 5 | id: string; 6 | error: string; 7 | } 8 | 9 | const ErrorMessageComponent: React.FC = ({ 10 | id, 11 | error, 12 | }) => { 13 | return ( 14 | 15 | {error} 16 | 17 | ); 18 | }; 19 | 20 | export { ErrorMessageComponent }; 21 | -------------------------------------------------------------------------------- /isucari/webapp/sql/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | set -o pipefail 4 | 5 | CURRENT_DIR=$(cd $(dirname $0);pwd) 6 | export MYSQL_HOST=${MYSQL_HOST:-127.0.0.1} 7 | export MYSQL_PORT=${MYSQL_PORT:-3306} 8 | export MYSQL_USER=${MYSQL_USER:-isucari} 9 | export MYSQL_DBNAME=${MYSQL_DBNAME:-isucari} 10 | export MYSQL_PWD=${MYSQL_PASS:-isucari} 11 | export LANG="C.UTF-8" 12 | cd $CURRENT_DIR 13 | 14 | cat 01_schema.sql 02_categories.sql initial.sql | mysql --defaults-file=/dev/null -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER $MYSQL_DBNAME 15 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/InternalServerContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import { Dispatch } from 'redux'; 4 | import { InternalServerErrorPage } from '../pages/error/InternalServerErrorPage'; 5 | 6 | const mapStateToProps = (state: AppState) => ({ 7 | message: state.error.errorMessage, 8 | }); 9 | const mapDispatchToProps = (dispatch: Dispatch) => ({}); 10 | 11 | export default connect( 12 | mapStateToProps, 13 | mapDispatchToProps, 14 | )(InternalServerErrorPage); 15 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ISUCARI 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /isucari/webapp/go/mysql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "time" 7 | ) 8 | 9 | func waitDB(db *sql.DB) { 10 | for { 11 | err := db.Ping() 12 | if err == nil { 13 | return 14 | } 15 | 16 | log.Printf("Failed to ping DB: %s", err) 17 | log.Println("Retrying...") 18 | time.Sleep(time.Second) 19 | } 20 | } 21 | 22 | func pollDB(db *sql.DB) { 23 | for { 24 | err := db.Ping() 25 | if err != nil { 26 | log.Printf("Failed to ping DB: %s", err) 27 | } 28 | 29 | time.Sleep(time.Second) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/BasePageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import BasePageComponent from '../components/BasePageComponent'; 4 | import { Dispatch } from 'redux'; 5 | 6 | const mapStateToProps = (state: AppState) => ({ 7 | loading: state.page.isLoading, 8 | alreadyLoaded: state.authStatus.checked, 9 | }); 10 | const mapDispatchToProps = (dispatch: Dispatch) => ({}); 11 | 12 | export default connect( 13 | mapStateToProps, 14 | mapDispatchToProps, 15 | )(BasePageComponent); 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/error/NotFoundPage/NotFoundPage.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { NotFoundPage } from '.'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | 6 | const stories = storiesOf('pages/NotFoundPage', module); 7 | 8 | stories.add('default', () => ( 9 | 10 | 11 | 12 | )); 13 | 14 | stories.add('with message', () => ( 15 | 16 | 17 | 18 | )); 19 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/LoadingButton/LoadingButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { LoadingButton } from '.'; 4 | 5 | const stories = storiesOf('components/LoadingButton', module); 6 | 7 | const mockProps = { 8 | onClick: (e: React.MouseEvent) => {}, 9 | }; 10 | 11 | stories 12 | .add('default', () => ( 13 | 14 | )) 15 | .add('loading', () => ( 16 | 17 | )); 18 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, Reducer, Store } from 'redux'; 2 | import { History } from 'history'; 3 | import { routerMiddleware } from 'connected-react-router'; 4 | import thunk from 'redux-thunk'; 5 | import { composeWithDevTools } from 'redux-devtools-extension'; 6 | import middleware from './middlewares'; 7 | 8 | export const getStore = (reducer: Reducer, history: History): Store => { 9 | return createStore( 10 | reducer, 11 | composeWithDevTools( 12 | applyMiddleware(thunk, routerMiddleware(history), ...middleware), 13 | ), 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/error/InternalServerErrorPage/InternalServerErrorPage.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { InternalServerErrorPage } from '.'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | 6 | const stories = storiesOf('pages/InternalServerErrorPage', module); 7 | 8 | stories.add('default', () => ( 9 | 10 | 11 | 12 | )); 13 | 14 | stories.add('with message', () => ( 15 | 16 | 17 | 18 | )); 19 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionLabel/TransactionLabel.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { TransactionLabel } from '.'; 4 | 5 | const stories = storiesOf('components/TransactionLabel', module); 6 | 7 | stories 8 | .add('on_sale', () => ) 9 | .add('trading', () => ) 10 | .add('sold_out', () => ) 11 | .add('stop', () => ) 12 | .add('cancel', () => ); 13 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/UserSettingPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import UserSettingPage from '../pages/UserSettingPage'; 4 | import { Dispatch } from 'redux'; 5 | 6 | const mapStateToProps = (state: AppState) => ({ 7 | id: state.authStatus.userId, 8 | accountName: state.authStatus.accountName, 9 | address: state.authStatus.address, 10 | numSellItems: state.authStatus.numSellItems, 11 | }); 12 | const mapDispatchToProps = (dispatch: Dispatch) => ({}); 13 | 14 | export default connect( 15 | mapStateToProps, 16 | mapDispatchToProps, 17 | )(UserSettingPage); 18 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/.storybook/config.tsx: -------------------------------------------------------------------------------- 1 | import {addDecorator, configure} from '@storybook/react'; 2 | import {MuiThemeProvider} from "@material-ui/core"; 3 | import {themeInstance} from "../src/theme"; 4 | import * as React from "react"; 5 | 6 | // automatically import all files ending in *.stories.tsx 7 | const req = require.context('../src', true, /\.stories\.tsx$/); 8 | function loadStories() { 9 | req.keys().forEach(req); 10 | } 11 | 12 | const BaseDecorator = storyFn => ( 13 | 14 | {storyFn()} 15 | 16 | ); 17 | addDecorator(BaseDecorator); 18 | 19 | configure(loadStories, module); 20 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/SnackBar/SnackBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { SnackBar } from '.'; 4 | 5 | const stories = storiesOf('components/SnackBar', module); 6 | 7 | stories 8 | .add('default', () => ( 9 | {}} 14 | /> 15 | )) 16 | .add('error', () => ( 17 | {}} 22 | /> 23 | )); 24 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/BuyCompleteContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import BuyCompletePage from '../pages/BuyComplete'; 3 | import { Dispatch } from 'redux'; 4 | import { push } from 'connected-react-router'; 5 | import { routes } from '../routes/Route'; 6 | 7 | const mapStateToProps = (state: any) => ({ 8 | itemId: state.viewingItem.item.id || 0, 9 | }); 10 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 11 | onClickTransaction: (itemId: number) => { 12 | dispatch(push(routes.transaction.getPath(itemId))); 13 | }, 14 | }); 15 | 16 | export default connect( 17 | mapStateToProps, 18 | mapDispatchToProps, 19 | )(BuyCompletePage); 20 | -------------------------------------------------------------------------------- /isucari/webapp/public/precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "bcbc5cb038cf9bc853f164a4822931e3", 4 | "url": "/index.html" 5 | }, 6 | { 7 | "revision": "0f448921f8b046650637", 8 | "url": "/static/css/main.19393e92.chunk.css" 9 | }, 10 | { 11 | "revision": "9f520e8bea2e3943530b", 12 | "url": "/static/js/2.ff6e1067.chunk.js" 13 | }, 14 | { 15 | "revision": "0f448921f8b046650637", 16 | "url": "/static/js/main.babc3d4d.chunk.js" 17 | }, 18 | { 19 | "revision": "42ac5946195a7306e2a5", 20 | "url": "/static/js/runtime~main.a8a9905a.js" 21 | } 22 | ]); -------------------------------------------------------------------------------- /isucari/webapp/perl/app.psgi: -------------------------------------------------------------------------------- 1 | use FindBin; 2 | use lib "$FindBin::Bin/extlib/lib/perl5"; 3 | use lib "$FindBin::Bin/lib"; 4 | use File::Basename; 5 | use Plack::Builder; 6 | use Isucari::Web; 7 | 8 | my $root_dir = File::Basename::dirname(__FILE__); 9 | 10 | my $app = Isucari::Web->psgi($root_dir); 11 | builder { 12 | enable 'ReverseProxy'; 13 | enable 'Session::Cookie', 14 | session_key => 'session-isucari', 15 | expires => 3600, 16 | secret => 'tagomoris'; 17 | enable 'Static', 18 | path => qr!^/(?:(?:static|upload|js)/|([^/]+)\.(?:js|png|ico)$|asset-manifest\.json$|manifest\.json$)!, 19 | root => $root_dir . '/public'; 20 | $app; 21 | }; 22 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/viewingItemReducer.ts: -------------------------------------------------------------------------------- 1 | import { ItemData } from '../dataObjects/item'; 2 | import { FETCH_ITEM_SUCCESS } from '../actions/fetchItemAction'; 3 | import { ActionTypes } from '../actions/actionTypes'; 4 | 5 | export interface ViewingItemState { 6 | item?: ItemData; 7 | } 8 | 9 | const initialState: ViewingItemState = {}; 10 | 11 | const viewingItem = ( 12 | state: ViewingItemState = initialState, 13 | action: ActionTypes, 14 | ): ViewingItemState => { 15 | switch (action.type) { 16 | case FETCH_ITEM_SUCCESS: 17 | return { ...state, item: action.payload.item }; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default viewingItem; 24 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/ItemBuyPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchItemAction } from '../actions/fetchItemAction'; 3 | import { AppState } from '../index'; 4 | import ItemBuyPage from '../pages/ItemBuyPage'; 5 | 6 | const mapStateToProps = (state: AppState) => ({ 7 | loading: !state.viewingItem.item, // 商品がstateにない場合はloadingにする 8 | item: state.viewingItem.item, 9 | errorType: state.error.errorType, 10 | }); 11 | const mapDispatchToProps = (dispatch: any) => ({ 12 | load: (itemId: string) => { 13 | dispatch(fetchItemAction(itemId)); 14 | }, 15 | }); 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps, 20 | )(ItemBuyPage); 21 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/BuyComplete.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BasePageContainer from '../containers/BasePageContainer'; 3 | import { Button } from '@material-ui/core'; 4 | 5 | type Props = { 6 | itemId: number; 7 | onClickTransaction: (itemId: number) => void; 8 | }; 9 | 10 | const BuyCompletePage: React.FC = ({ itemId, onClickTransaction }) => ( 11 | 12 |
購入が完了しました
13 | 22 |
23 | ); 24 | 25 | export default BuyCompletePage; 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TimelineLoading/TimelineLoading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CircularProgress, Theme } from '@material-ui/core'; 3 | import makeStyles from '@material-ui/core/styles/makeStyles'; 4 | 5 | const useStyles = makeStyles((theme: Theme) => ({ 6 | root: { 7 | display: 'flex', 8 | justifyContent: 'center', 9 | }, 10 | loader: { 11 | margin: theme.spacing(3), 12 | }, 13 | })); 14 | 15 | const TimelineLoading: React.FC<{}> = () => { 16 | const classes = useStyles(); 17 | 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }; 24 | 25 | export { TimelineLoading }; 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/BuyerTransactionContainer.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from '../index'; 2 | import { connect } from 'react-redux'; 3 | import { TransactionBuyer } from '../components/TransactionBuyer'; 4 | import { postCompleteAction } from '../actions/postCompleteAction'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | import { ActionTypes } from '../actions/actionTypes'; 7 | 8 | const mapStateToProps = (state: AppState) => ({}); 9 | const mapDispatchToProps = ( 10 | dispatch: ThunkDispatch, 11 | ) => ({ 12 | postComplete: (itemId: number) => { 13 | dispatch(postCompleteAction(itemId)); 14 | }, 15 | }); 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps, 20 | )(TransactionBuyer); 21 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/SnackBarContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import { Dispatch } from 'redux'; 4 | import { closeSnackBarAction } from '../actions/snackBarAction'; 5 | import { SnackBar } from '../components/SnackBar'; 6 | import * as React from 'react'; 7 | 8 | const mapStateToProps = (state: AppState) => ({ 9 | open: state.snackBar.available, 10 | message: state.snackBar.reason, 11 | variant: state.snackBar.variant, 12 | }); 13 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 14 | handleClose(event: React.MouseEvent) { 15 | dispatch(closeSnackBarAction()); 16 | }, 17 | }); 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps, 22 | )(SnackBar); 23 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/categoriesReducer.ts: -------------------------------------------------------------------------------- 1 | import { CategorySimple } from '../dataObjects/category'; 2 | import { FETCH_SETTINGS_SUCCESS } from '../actions/settingsAction'; 3 | import { ActionTypes } from '../actions/actionTypes'; 4 | 5 | export interface CategoriesState { 6 | categories: CategorySimple[]; 7 | } 8 | 9 | const initialState: CategoriesState = { 10 | categories: [], 11 | }; 12 | 13 | const categories = ( 14 | state: CategoriesState = initialState, 15 | action: ActionTypes, 16 | ): CategoriesState => { 17 | switch (action.type) { 18 | case FETCH_SETTINGS_SUCCESS: 19 | return { 20 | categories: action.payload.settings.categories, 21 | }; 22 | default: 23 | return { ...state }; 24 | } 25 | }; 26 | 27 | export default categories; 28 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/SignInFormContainer.tsx: -------------------------------------------------------------------------------- 1 | import SignInPageFormComponent from '../components/SignInFormComponent'; 2 | import { connect } from 'react-redux'; 3 | import { postLoginAction } from '../actions/authenticationActions'; 4 | import { AppState } from '../index'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | import { ActionTypes } from '../actions/actionTypes'; 7 | 8 | const mapStateToProps = (state: AppState) => ({}); 9 | const mapDispatchToProps = ( 10 | dispatch: ThunkDispatch, 11 | ) => ({ 12 | onSubmit: (accountName: string, password: string) => { 13 | dispatch(postLoginAction(accountName, password)); 14 | }, 15 | }); 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps, 20 | )(SignInPageFormComponent); 21 | -------------------------------------------------------------------------------- /.bash_profile: -------------------------------------------------------------------------------- 1 | # BEGIN ANSIBLE MANAGED BLOCK go 2 | export PATH=/home/isucon/local/go/bin:/home/isucon/go/bin:$PATH 3 | export GOROOT=/home/isucon/local/go 4 | # END ANSIBLE MANAGED BLOCK go 5 | # BEGIN ANSIBLE MANAGED BLOCK perl 6 | export PATH=/home/isucon/local/perl/bin:$PATH 7 | # END ANSIBLE MANAGED BLOCK perl 8 | # BEGIN ANSIBLE MANAGED BLOCK php 9 | export PATH=/home/isucon/local/php/bin:$PATH 10 | # END ANSIBLE MANAGED BLOCK php 11 | # BEGIN ANSIBLE MANAGED BLOCK ruby 12 | export PATH=/home/isucon/local/ruby/bin:$PATH 13 | # END ANSIBLE MANAGED BLOCK ruby 14 | # BEGIN ANSIBLE MANAGED BLOCK python 15 | export PATH=/home/isucon/local/python/bin:$PATH 16 | # END ANSIBLE MANAGED BLOCK python 17 | # BEGIN ANSIBLE MANAGED BLOCK nodejs 18 | export PATH=/home/isucon/local/node/bin:$PATH 19 | # END ANSIBLE MANAGED BLOCK nodejs 20 | -------------------------------------------------------------------------------- /isucari/webapp/php/src/middleware.php: -------------------------------------------------------------------------------- 1 | add( 10 | new \Slim\Middleware\Session([ 11 | 'name' => 'session-isucari', 12 | ]) 13 | ); 14 | 15 | // logging 16 | // $app->add(function (Request $request, Response $response, callable $next) { 17 | // $route = $request->getAttribute('route'); 18 | // $this->logger->info($request->getMethod() . ' ' . $route->getPattern(), [$route->getArguments()]); 19 | // $response = $next($request, $response); 20 | // $this->logger->info($response->getStatusCode() . ' ' . $response->getReasonPhrase(), [(string)$response->getBody()]); 21 | 22 | // return $response; 23 | // }); 24 | }; 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/SignUpFormContainer.tsx: -------------------------------------------------------------------------------- 1 | import SignUpPageFormComponent from '../components/SignUpFormComponent'; 2 | import { connect } from 'react-redux'; 3 | import { postRegisterAction } from '../actions/registerAction'; 4 | import { RegisterReq } from '../types/appApiTypes'; 5 | import { AppState } from '../index'; 6 | import { ThunkDispatch } from 'redux-thunk'; 7 | import { ActionTypes } from '../actions/actionTypes'; 8 | 9 | const mapStateToProps = (state: AppState) => ({}); 10 | const mapDispatchToProps = ( 11 | dispatch: ThunkDispatch, 12 | ) => ({ 13 | register: (params: RegisterReq) => { 14 | dispatch(postRegisterAction(params)); 15 | }, 16 | }); 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps, 21 | )(SignUpPageFormComponent); 22 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/SignInPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core'; 3 | import SignInFormContainer from '../containers/SignInFormContainer'; 4 | import BasePageContainer from '../containers/BasePageContainer'; 5 | 6 | const useStyles = makeStyles((theme: Theme) => ({ 7 | paper: { 8 | marginTop: theme.spacing(1), 9 | display: 'flex', 10 | flexDirection: 'column', 11 | alignItems: 'center', 12 | }, 13 | })); 14 | 15 | type Props = {}; 16 | 17 | const SignInPage: React.FC = () => { 18 | const classes = useStyles(); 19 | 20 | return ( 21 | 22 |
23 | 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default SignInPage; 30 | -------------------------------------------------------------------------------- /isucari/webapp/public/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.19393e92.chunk.css", 4 | "main.js": "/static/js/main.babc3d4d.chunk.js", 5 | "main.js.map": "/static/js/main.babc3d4d.chunk.js.map", 6 | "runtime~main.js": "/static/js/runtime~main.a8a9905a.js", 7 | "runtime~main.js.map": "/static/js/runtime~main.a8a9905a.js.map", 8 | "static/js/2.ff6e1067.chunk.js": "/static/js/2.ff6e1067.chunk.js", 9 | "static/js/2.ff6e1067.chunk.js.map": "/static/js/2.ff6e1067.chunk.js.map", 10 | "index.html": "/index.html", 11 | "precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js": "/precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js", 12 | "service-worker.js": "/service-worker.js", 13 | "static/css/main.19393e92.chunk.css.map": "/static/css/main.19393e92.chunk.css.map" 14 | } 15 | } -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actionHelper/ajaxErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError } from '../errors/NotFoundError'; 2 | import { 3 | internalServerError, 4 | InternalServerErrorAction, 5 | notFoundError, 6 | NotFoundErrorAction, 7 | } from '../actions/errorAction'; 8 | import { InternalServerError } from '../errors/InternalServerError'; 9 | import { Action } from 'redux'; 10 | 11 | export async function ajaxErrorHandler>( 12 | err: Error, 13 | actionCreate: (message: string) => T, 14 | ): Promise { 15 | if (err instanceof NotFoundError) { 16 | return notFoundError(err.message); 17 | } 18 | 19 | if (err instanceof InternalServerError) { 20 | return internalServerError(err.message); 21 | } 22 | 23 | return actionCreate(err.message); 24 | } 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actionHelper/responseChecker.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRes } from '../types/appApiTypes'; 2 | import { NotFoundError } from '../errors/NotFoundError'; 3 | import { InternalServerError } from '../errors/InternalServerError'; 4 | import { AppResponseError } from '../errors/AppResponseError'; 5 | 6 | /** 7 | * checking response from application and throw error if it's necessary 8 | */ 9 | export async function checkAppResponse(response: Response) { 10 | if (!response.ok) { 11 | const errRes: ErrorRes = await response.json(); 12 | 13 | if (response.status === 404) { 14 | throw new NotFoundError(errRes.error); 15 | } 16 | 17 | if (response.status >= 500) { 18 | throw new InternalServerError(errRes.error); 19 | } 20 | 21 | throw new AppResponseError(errRes.error, response); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/errorAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | export const NOT_FOUND_ERROR = 'NOT_FOUND_ERROR'; 4 | export const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; 5 | 6 | export type ErrorActions = NotFoundErrorAction | InternalServerErrorAction; 7 | 8 | export interface NotFoundErrorAction extends Action { 9 | message: string; 10 | } 11 | 12 | export function notFoundError(message: string): NotFoundErrorAction { 13 | return { type: NOT_FOUND_ERROR, message }; 14 | } 15 | 16 | export interface InternalServerErrorAction 17 | extends Action { 18 | message: string; 19 | } 20 | 21 | export function internalServerError( 22 | message: string, 23 | ): InternalServerErrorAction { 24 | return { type: INTERNAL_SERVER_ERROR, message }; 25 | } 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/buyPageReducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BUY_FAIL, 3 | BUY_START, 4 | BUY_SUCCESS, 5 | USING_CARD_FAIL, 6 | } from '../actions/buyAction'; 7 | import { ActionTypes } from '../actions/actionTypes'; 8 | 9 | export interface BuyPageState { 10 | loadingBuy: boolean; 11 | } 12 | 13 | const initialState: BuyPageState = { 14 | loadingBuy: false, 15 | }; 16 | 17 | const buyPage = ( 18 | state: BuyPageState = initialState, 19 | action: ActionTypes, 20 | ): BuyPageState => { 21 | switch (action.type) { 22 | case BUY_START: 23 | return { ...state, loadingBuy: true }; 24 | case BUY_SUCCESS: 25 | case BUY_FAIL: 26 | case USING_CARD_FAIL: 27 | return { ...state, loadingBuy: false }; 28 | default: 29 | return { ...state }; 30 | } 31 | }; 32 | 33 | export default buyPage; 34 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { Provider } from 'react-redux'; 6 | import { createBrowserHistory } from 'history'; 7 | import { ConnectedRouter } from 'connected-react-router'; 8 | import { getStore } from './configureStore'; 9 | import createRootReducer from './reducers/index'; 10 | 11 | const history = createBrowserHistory(); 12 | const rootReducers = createRootReducer(history); 13 | const store = getStore(rootReducers, history); 14 | 15 | export type AppState = ReturnType; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById('root'), 24 | ); 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/SignUpPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import makeStyles from '@material-ui/core/styles/makeStyles'; 3 | import SignUpFormContainer from '../containers/SignUpFormContainer'; 4 | import BasePageContainer from '../containers/BasePageContainer'; 5 | import { Theme } from '@material-ui/core'; 6 | 7 | const useStyles = makeStyles((theme: Theme) => ({ 8 | paper: { 9 | marginTop: theme.spacing(1), 10 | display: 'flex', 11 | flexDirection: 'column', 12 | alignItems: 'center', 13 | }, 14 | })); 15 | 16 | const SignUpPage: React.FC = () => { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 21 |
22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default SignUpPage; 29 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/SellingButtonContainer.tsx: -------------------------------------------------------------------------------- 1 | import { push } from 'connected-react-router'; 2 | import { SellingButtonComponent } from '../components/SellingButtonComponent'; 3 | import { connect } from 'react-redux'; 4 | import { routes } from '../routes/Route'; 5 | import * as React from 'react'; 6 | import { AppState } from '../index'; 7 | import { ThunkDispatch } from 'redux-thunk'; 8 | import { AnyAction } from 'redux'; 9 | 10 | const mapStateToProps = (state: AppState) => ({}); 11 | 12 | const mapDispatchToProps = ( 13 | dispatch: ThunkDispatch, 14 | ) => ({ 15 | onClick: (e: React.MouseEvent) => { 16 | e.preventDefault(); 17 | dispatch(push(routes.sell.path)); 18 | }, 19 | }); 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps, 24 | )(SellingButtonComponent); 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/AuthContainer.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from '../index'; 2 | import { connect } from 'react-redux'; 3 | import { AuthRoute } from '../components/Route/AuthRoute'; 4 | import { fetchSettings } from '../actions/settingsAction'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | import { AnyAction } from 'redux'; 7 | 8 | const mapStateToProps = (state: AppState) => ({ 9 | isLoggedIn: !!state.authStatus.userId, 10 | loading: !state.authStatus.checked, 11 | alreadyLoaded: state.authStatus.checked, 12 | error: state.error.errorMessage, 13 | }); 14 | const mapDispatchToProps = ( 15 | dispatch: ThunkDispatch, 16 | ) => ({ 17 | load: () => { 18 | dispatch(fetchSettings()); 19 | }, 20 | }); 21 | 22 | export default connect( 23 | mapStateToProps, 24 | mapDispatchToProps, 25 | )(AuthRoute); 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/httpClients/paymentClient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP client for payment service 3 | */ 4 | class PaymentClient { 5 | private baseURL?: string; 6 | private defaultHeaders: HeadersInit = {}; 7 | 8 | async post(path: string, params?: Object): Promise { 9 | let requestOption: RequestInit = { 10 | method: 'POST', 11 | mode: 'cors', 12 | headers: Object.assign({}, this.defaultHeaders, { 13 | 'Content-Type': 'application/json', 14 | }), 15 | credentials: 'same-origin', 16 | }; 17 | 18 | if (params) { 19 | requestOption.body = JSON.stringify(params); 20 | } 21 | 22 | return await fetch(`${this.baseURL}${path}`, requestOption); 23 | } 24 | 25 | public setBaseURL(baseURL: string) { 26 | this.baseURL = baseURL; 27 | } 28 | } 29 | 30 | export default new PaymentClient(); 31 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/NonAuthContainer.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from '../index'; 2 | import { AnyAction } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { NonAuthRoute } from '../components/Route/NonAuthRoute'; 5 | import { fetchSettings } from '../actions/settingsAction'; 6 | import { ThunkDispatch } from 'redux-thunk'; 7 | 8 | const mapStateToProps = (state: AppState) => ({ 9 | isLoggedIn: !!state.authStatus.userId, 10 | loading: !state.authStatus.checked, 11 | alreadyLoaded: state.authStatus.checked, 12 | error: state.error.errorMessage, 13 | }); 14 | const mapDispatchToProps = ( 15 | dispatch: ThunkDispatch, 16 | ) => ({ 17 | load: () => { 18 | dispatch(fetchSettings()); 19 | }, 20 | }); 21 | 22 | export default connect( 23 | mapStateToProps, 24 | mapDispatchToProps, 25 | )(NonAuthRoute); 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionBuyer/TransactionBuyer.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { TransactionBuyer, Props } from '.'; 4 | 5 | const stories = storiesOf('components/TransactionBuyer', module); 6 | 7 | const mockProps: Props = { 8 | itemId: 1, 9 | postComplete: (itemId: number) => {}, 10 | transactionStatus: 'wait_shipping', 11 | shippingStatus: 'initial', 12 | }; 13 | 14 | stories 15 | .add('wait shipping', () => ) 16 | .add('wait pickup', () => ( 17 | 18 | )) 19 | .add('wait done', () => ( 20 | 21 | )) 22 | .add('done', () => ( 23 | 24 | )); 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/ItemListPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import ItemListPage from '../pages/ItemListPage'; 4 | import { fetchTimelineAction } from '../actions/fetchTimelineAction'; 5 | 6 | const mapStateToProps = (state: AppState) => { 7 | return { 8 | items: state.timeline.items, 9 | hasNext: state.timeline.hasNext, 10 | errorType: state.error.errorType, 11 | loading: state.page.isTimelineLoading, 12 | }; 13 | }; 14 | const mapDispatchToProps = (dispatch: any) => ({ 15 | load: () => { 16 | dispatch(fetchTimelineAction()); 17 | }, 18 | loadMore: (createdAt: number, itemId: number, page: number) => { 19 | dispatch(fetchTimelineAction(createdAt, itemId)); 20 | }, 21 | }); 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps, 26 | )(ItemListPage); 27 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/TransactionContainer.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from '../index'; 2 | import { Dispatch } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { push } from 'connected-react-router'; 5 | import { routes } from '../routes/Route'; 6 | import { TransactionComponent } from '../components/TransactionComponent'; 7 | import { TransactionItem } from '../dataObjects/item'; 8 | 9 | const mapStateToProps = (state: AppState) => ({}); 10 | 11 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 12 | onClickCard(item: TransactionItem) { 13 | if (item.status === 'on_sale') { 14 | dispatch(push(routes.item.getPath(item.id))); 15 | return; 16 | } 17 | 18 | dispatch(push(routes.transaction.getPath(item.id))); 19 | }, 20 | }); 21 | 22 | export default connect( 23 | mapStateToProps, 24 | mapDispatchToProps, 25 | )(TransactionComponent); 26 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/viewingUserReducer.ts: -------------------------------------------------------------------------------- 1 | import { UserData } from '../dataObjects/user'; 2 | import { FETCH_USER_PAGE_DATA_SUCCESS } from '../actions/fetchUserPageDataAction'; 3 | import { ActionTypes } from '../actions/actionTypes'; 4 | import { PATH_NAME_CHANGE } from '../actions/locationChangeAction'; 5 | 6 | // ユーザページに表示するユーザ情報のstate 7 | export interface ViewingUserState { 8 | user?: UserData; 9 | } 10 | 11 | const initialState: ViewingUserState = {}; 12 | 13 | const viewingUser = ( 14 | state: ViewingUserState = initialState, 15 | action: ActionTypes, 16 | ): ViewingUserState => { 17 | switch (action.type) { 18 | case PATH_NAME_CHANGE: 19 | return initialState; 20 | case FETCH_USER_PAGE_DATA_SUCCESS: 21 | return { ...state, user: action.payload.user }; 22 | default: 23 | return { ...state }; 24 | } 25 | }; 26 | 27 | export default viewingUser; 28 | -------------------------------------------------------------------------------- /.profile: -------------------------------------------------------------------------------- 1 | # ~/.profile: executed by the command interpreter for login shells. 2 | # This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login 3 | # exists. 4 | # see /usr/share/doc/bash/examples/startup-files for examples. 5 | # the files are located in the bash-doc package. 6 | 7 | # the default umask is set in /etc/profile; for setting the umask 8 | # for ssh logins, install and configure the libpam-umask package. 9 | #umask 022 10 | 11 | # if running bash 12 | if [ -n "$BASH_VERSION" ]; then 13 | # include .bashrc if it exists 14 | if [ -f "$HOME/.bashrc" ]; then 15 | . "$HOME/.bashrc" 16 | fi 17 | fi 18 | 19 | # set PATH so it includes user's private bin if it exists 20 | if [ -d "$HOME/bin" ] ; then 21 | PATH="$HOME/bin:$PATH" 22 | fi 23 | 24 | # set PATH so it includes user's private bin if it exists 25 | if [ -d "$HOME/.local/bin" ] ; then 26 | PATH="$HOME/.local/bin:$PATH" 27 | fi 28 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/ItemBuyFormContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ItemBuyFormComponent from '../components/ItemBuyFormComponent'; 3 | import { buyItemAction } from '../actions/buyAction'; 4 | import { AppState } from '../index'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | import { ActionTypes } from '../actions/actionTypes'; 7 | 8 | const mapStateToProps = (state: AppState) => ({ 9 | item: state.viewingItem.item, 10 | errors: state.formError.buyFormError, 11 | loadingBuy: state.buyPage.loadingBuy, 12 | }); 13 | const mapDispatchToProps = ( 14 | dispatch: ThunkDispatch, 15 | ) => ({ 16 | onBuyAction: (itemId: number, cardNumber: string) => { 17 | dispatch(buyItemAction(itemId, cardNumber)); 18 | }, 19 | }); 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps, 24 | )(ItemBuyFormComponent); 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/TransactionPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import TransactionPage from '../pages/TransactionPage'; 4 | import { ThunkDispatch } from 'redux-thunk'; 5 | import { AnyAction } from 'redux'; 6 | import { fetchItemAction } from '../actions/fetchItemAction'; 7 | 8 | const mapStateToProps = (state: AppState) => ({ 9 | loading: state.page.isItemLoading, 10 | item: state.viewingItem.item, 11 | auth: { 12 | userId: state.authStatus.userId || 0, 13 | }, 14 | errorType: state.error.errorType, 15 | }); 16 | const mapDispatchToProps = ( 17 | dispatch: ThunkDispatch, 18 | ) => ({ 19 | load: (itemId: string) => { 20 | dispatch(fetchItemAction(itemId)); 21 | }, 22 | }); 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps, 27 | )(TransactionPage); 28 | -------------------------------------------------------------------------------- /isucari/webapp/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isucon9-qualify/isucari", 3 | "description": "A sample app for Isucon9", 4 | "keywords": ["isucon", "isucon9"], 5 | "homepage": "http://isucon.net/", 6 | "license": "MIT", 7 | "require": { 8 | "php": "~7.2", 9 | "ext-pdo": "*", 10 | "bryanjhv/slim-session": "^3.6", 11 | "guzzlehttp/guzzle": "~6.0", 12 | "monolog/monolog": "^1.17", 13 | "slim/php-view": "^2.0", 14 | "slim/slim": "^3.1" 15 | }, 16 | "require-dev": { 17 | "friendsofphp/php-cs-fixer": "^2.15" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "App\\": "src/App" 22 | } 23 | }, 24 | "config": { 25 | "process-timeout": 0, 26 | "sort-packages": true 27 | }, 28 | "scripts": { 29 | "start": "php -S 127.0.0.1:8000 -t public public/index.php" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/SellPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core'; 3 | import SellFormContainer from '../containers/SellFormContainer'; 4 | import { ErrorProps, PageComponentWithError } from '../hoc/withBaseComponent'; 5 | import BasePageContainer from '../containers/BasePageContainer'; 6 | 7 | const useStyles = makeStyles((theme: Theme) => ({ 8 | paper: { 9 | marginTop: theme.spacing(1), 10 | display: 'flex', 11 | flexDirection: 'column', 12 | alignItems: 'center', 13 | }, 14 | })); 15 | 16 | type Props = {} & ErrorProps; 17 | 18 | const SellPage: React.FC = () => { 19 | const classes = useStyles(); 20 | 21 | return ( 22 | 23 |
24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default PageComponentWithError()(SellPage); 31 | -------------------------------------------------------------------------------- /isucari/webapp/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | backports (3.15.0) 5 | bcrypt (3.1.13) 6 | multi_json (1.13.1) 7 | mustermann (1.0.3) 8 | mysql2 (0.5.2) 9 | mysql2-cs-bind (0.0.7) 10 | mysql2 11 | nio4r (2.5.1) 12 | puma (4.1.0) 13 | nio4r (~> 2.0) 14 | rack (2.0.7) 15 | rack-protection (2.0.7) 16 | rack 17 | sinatra (2.0.7) 18 | mustermann (~> 1.0) 19 | rack (~> 2.0) 20 | rack-protection (= 2.0.7) 21 | tilt (~> 2.0) 22 | sinatra-contrib (2.0.7) 23 | backports (>= 2.8.2) 24 | multi_json 25 | mustermann (~> 1.0) 26 | rack-protection (= 2.0.7) 27 | sinatra (= 2.0.7) 28 | tilt (~> 2.0) 29 | tilt (2.0.9) 30 | 31 | PLATFORMS 32 | ruby 33 | 34 | DEPENDENCIES 35 | bcrypt 36 | mysql2 37 | mysql2-cs-bind 38 | puma 39 | sinatra 40 | sinatra-contrib 41 | 42 | BUNDLED WITH 43 | 2.0.1 44 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/SellerTransactionContainer.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from '../index'; 2 | import { connect } from 'react-redux'; 3 | import { TransactionSeller } from '../components/TransactionSeller'; 4 | import { postShippedDoneAction } from '../actions/postShippedDoneAction'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | import { postShippedAction } from '../actions/postShippedAction'; 7 | import { ActionTypes } from '../actions/actionTypes'; 8 | 9 | const mapStateToProps = (state: AppState) => ({}); 10 | const mapDispatchToProps = ( 11 | dispatch: ThunkDispatch, 12 | ) => ({ 13 | postShipped: (itemId: number) => { 14 | dispatch(postShippedAction(itemId)); 15 | }, 16 | postShippedDone: (itemId: number) => { 17 | dispatch(postShippedDoneAction(itemId)); 18 | }, 19 | }); 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps, 24 | )(TransactionSeller); 25 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/SellingButtonComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Fab from '@material-ui/core/Fab/Fab'; 3 | import makeStyles from '@material-ui/core/styles/makeStyles'; 4 | import { Edit } from '@material-ui/icons'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | fab: { 8 | margin: theme.spacing(1), 9 | position: 'fixed', 10 | top: 'auto', 11 | bottom: '30px', 12 | right: '30px', 13 | width: '100px', 14 | height: '100px', 15 | }, 16 | })); 17 | 18 | interface SellingButtonComponentProps { 19 | onClick: (e: React.MouseEvent) => void; 20 | } 21 | 22 | const SellingButtonComponent: React.FC = ({ 23 | onClick, 24 | }) => { 25 | const classes = useStyles(); 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export { SellingButtonComponent }; 35 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Item/Item.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { Item } from '.'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | import { ItemStatus } from '../../dataObjects/item'; 6 | import { GridList } from '@material-ui/core'; 7 | 8 | const stories = storiesOf('components/Item', module); 9 | 10 | const mockProps = { 11 | itemId: 1, 12 | imageUrl: 'https://i.gyazo.com/8560fce19556b64c95ad091350910184.jpg', 13 | title: 'サンプル', 14 | price: 10000, 15 | status: 'on_sale' as ItemStatus, 16 | }; 17 | 18 | stories 19 | .add('default', () => ( 20 | 21 | 22 | 23 | 24 | 25 | )) 26 | .add('sold out', () => ( 27 | 28 | 29 | 30 | 31 | 32 | )); 33 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionSeller/TransactionSeller.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { TransactionSeller, Props } from '.'; 4 | 5 | const stories = storiesOf('components/TransactionSeller', module); 6 | 7 | const mockProps: Props = { 8 | itemId: 1, 9 | transactionEvidenceId: 1, 10 | postShipped: (itemId: number) => {}, 11 | postShippedDone: (itemId: number) => {}, 12 | transactionStatus: 'wait_shipping', 13 | shippingStatus: 'initial', 14 | }; 15 | 16 | stories 17 | .add('wait shipping', () => ) 18 | .add('wait pickup', () => ( 19 | 20 | )) 21 | .add('wait done', () => ( 22 | 23 | )) 24 | .add('done', () => ( 25 | 26 | )); 27 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Header/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { Header } from '.'; 4 | 5 | const stories = storiesOf('components/Header', module); 6 | 7 | const mockProps = { 8 | isLoggedIn: false, 9 | ownUserId: 1111, 10 | categories: [ 11 | { 12 | id: 2, 13 | parentId: 1, 14 | categoryName: 'カテゴリ1', 15 | }, 16 | { 17 | id: 3, 18 | parentId: 1, 19 | categoryName: 'カテゴリ2', 20 | }, 21 | { 22 | id: 4, 23 | parentId: 1, 24 | categoryName: 'カテゴリ3', 25 | }, 26 | ], 27 | goToTopPage: () => {}, 28 | goToUserPage: (userId: number) => {}, 29 | goToSettingPage: () => {}, 30 | goToCategoryItemList: (categoryId: number) => {}, 31 | onClickTitle: (isLoggedIn: boolean) => {}, 32 | }; 33 | 34 | stories 35 | .add('non sign in', () =>
) 36 | .add('signed in', () =>
); 37 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/LoadingComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, MuiThemeProvider, Theme } from '@material-ui/core/styles'; 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import { themeInstance } from '../theme'; 5 | 6 | const useStyles = makeStyles((theme: Theme) => ({ 7 | progress: { 8 | top: 0, 9 | bottom: 0, 10 | right: 0, 11 | left: 0, 12 | margin: 'auto', 13 | position: 'absolute', 14 | }, 15 | })); 16 | 17 | const LoadingComponent: React.FC = () => { 18 | const classes = useStyles(); 19 | 20 | // MEMO: Wrap component by MuiThemeProvider again to ignore this bug. https://github.com/mui-org/material-ui/issues/14044 21 | return ( 22 | 23 | 28 | 29 | ); 30 | }; 31 | 32 | export default LoadingComponent; 33 | -------------------------------------------------------------------------------- /isucari/webapp/public/static/css/main.19393e92.chunk.css: -------------------------------------------------------------------------------- 1 | body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{text-align:center}.App-logo{-webkit-animation:App-logo-spin 20s linear infinite;animation:App-logo-spin 20s linear infinite;height:40vmin;pointer-events:none}.App-header{background-color:#282c34;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin);color:#fff}.App-link{color:#61dafb}@-webkit-keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}} 2 | /*# sourceMappingURL=main.19393e92.chunk.css.map */ -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/hoc/withBaseComponent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorType, 3 | InternalServerError, 4 | NotFoundError, 5 | } from '../reducers/errorReducer'; 6 | import { branch, renderComponent, withProps, compose } from 'recompose'; 7 | import NotFoundContainer from '../containers/NotFoundContainer'; 8 | import InternalServerContainer from '../containers/InternalServerContainer'; 9 | 10 | export interface ErrorProps { 11 | errorType: ErrorType; 12 | } 13 | 14 | type BaseProps = ErrorProps; 15 | 16 | export const PageComponentWithError = () => 17 | compose( 18 | withProps((props: Props) => ({ 19 | errorType: props.errorType, 20 | })), 21 | branch( 22 | (props: BaseProps) => props.errorType === NotFoundError, 23 | renderComponent(NotFoundContainer), 24 | ), 25 | branch( 26 | (props: BaseProps) => props.errorType === InternalServerError, 27 | renderComponent(InternalServerContainer), 28 | ), 29 | ); 30 | -------------------------------------------------------------------------------- /isucari/webapp/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isucon9q", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "ts-node --files ./index.ts", 9 | "dev": "ts-node-dev --files ./index.ts", 10 | "test": "tsc --noEmit", 11 | "update-dependencies": "rm -rf node_modules && npm install && npm dedupe" 12 | }, 13 | "engines": { 14 | "node": ">= 12.9.0" 15 | }, 16 | "license": "ISC", 17 | "devDependencies": { 18 | "ts-node": "latest", 19 | "ts-node-dev": "^1.0.0-pre.42", 20 | "typescript": "latest" 21 | }, 22 | "dependencies": { 23 | "@types/axios": "^0.14.0", 24 | "@types/bcrypt": "^3.0.0", 25 | "@types/node": "^12", 26 | "axios": "^0.19.0", 27 | "bcrypt": "^3.0.6", 28 | "fastify": "^2.7", 29 | "fastify-cookie": "^3.1", 30 | "fastify-multipart": "^1.0.2", 31 | "fastify-mysql": "^0.3", 32 | "fastify-plugin": "^1.6", 33 | "fastify-static": "^2.5", 34 | "mysql2": "^1.6", 35 | "trace-error": "^1.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/ItemEditPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchItemAction } from '../actions/fetchItemAction'; 3 | import { AppState } from '../index'; 4 | import { postItemEditAction } from '../actions/postItemEditAction'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | import { AnyAction } from 'redux'; 7 | import ItemEditPage from '../pages/ItemEditPage'; 8 | 9 | const mapStateToProps = (state: AppState) => ({ 10 | loading: state.page.isItemLoading, 11 | item: state.viewingItem.item, 12 | errorType: state.error.errorType, 13 | formError: state.formError.itemEditFormError, 14 | }); 15 | const mapDispatchToProps = ( 16 | dispatch: ThunkDispatch, 17 | ) => ({ 18 | load: (itemId: string) => { 19 | dispatch(fetchItemAction(itemId)); 20 | }, 21 | onClickEdit: (itemId: number, price: number) => { 22 | dispatch(postItemEditAction(itemId, price)); 23 | }, 24 | }); 25 | 26 | export default connect( 27 | mapStateToProps, 28 | mapDispatchToProps, 29 | )(ItemEditPage); 30 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/middlewares/checkLocationChange.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, Middleware, MiddlewareAPI } from 'redux'; 2 | import { AppState } from '../index'; 3 | import { LOCATION_CHANGE } from 'connected-react-router'; 4 | import { pathNameChangeAction } from '../actions/locationChangeAction'; 5 | import { ActionTypes } from '../actions/actionTypes'; 6 | 7 | // react-routerのページ遷移発火時、pathnameが変わった場合は独自のactionを発火する 8 | const checkLocationChange: Middleware = ( 9 | store: MiddlewareAPI, 10 | ) => (next: Dispatch) => (action: ActionTypes): any => { 11 | const { getState, dispatch } = store; 12 | if (action.type !== LOCATION_CHANGE) { 13 | return next(action); 14 | } 15 | 16 | const { router } = getState(); 17 | const currentPath = router.location.pathname; 18 | const nextPath = action.payload.location.pathname; 19 | 20 | if (currentPath === nextPath) { 21 | return next(action); 22 | } 23 | 24 | dispatch(pathNameChangeAction()); 25 | return next(action); 26 | }; 27 | 28 | export default checkLocationChange; 29 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemImage/ItemImage.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { ItemImage } from '.'; 4 | 5 | const stories = storiesOf('components/ItemImage', module); 6 | const mockUrl = 'https://i.gyazo.com/8560fce19556b64c95ad091350910184.jpg'; 7 | 8 | stories 9 | .add('default', () => ( 10 | 11 | )) 12 | .add('sold out', () => ( 13 | 19 | )) 20 | .add('image not found', () => ( 21 | 27 | )) 28 | .add('default(500px)', () => ( 29 | 30 | )) 31 | .add('sold out(500px)', () => ( 32 | 33 | )); 34 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import authStatus from './authStatusReducer'; 3 | import formError from './formErrorReducer'; 4 | import viewingItem from './viewingItemReducer'; 5 | import viewingUser from './viewingUserReducer'; 6 | import error from './errorReducer'; 7 | import page from './pageReducuer'; 8 | import snackBar from './snackBarReducer'; 9 | import buyPage from './buyPageReducer'; 10 | import categories from './categoriesReducer'; 11 | import timeline from './timelineReducer'; 12 | import transactions from './transactionsReducer'; 13 | import userItems from './userItemsReducer'; 14 | import { connectRouter } from 'connected-react-router'; 15 | import { History } from 'history'; 16 | 17 | export default (history: History) => 18 | combineReducers({ 19 | router: connectRouter(history), 20 | authStatus, 21 | formError, 22 | viewingItem, 23 | viewingUser, 24 | error, 25 | page, 26 | snackBar, 27 | buyPage, 28 | categories, 29 | timeline, 30 | transactions, 31 | userItems, 32 | }); 33 | -------------------------------------------------------------------------------- /isucari/webapp/sql/02_categories.sql: -------------------------------------------------------------------------------- 1 | use `isucari`; 2 | 3 | INSERT INTO categories (`id`,`parent_id`,`category_name`) VALUES 4 | (1,0,"ソファー"), 5 | (2,1,"一人掛けソファー"), 6 | (3,1,"二人掛けソファー"), 7 | (4,1,"コーナーソファー"), 8 | (5,1,"二段ソファー"), 9 | (6,1,"ソファーベッド"), 10 | (10, 0,"家庭用チェア"), 11 | (11,10,"スツール"), 12 | (12,10,"クッションスツール"), 13 | (13,10,"ダイニングチェア"), 14 | (14,10,"リビングチェア"), 15 | (15,10,"カウンターチェア"), 16 | (20, 0,"キッズチェア"), 17 | (21,20,"学習チェア"), 18 | (22,20,"ベビーソファ"), 19 | (23,20,"キッズハイチェア"), 20 | (24,20,"テーブルチェア"), 21 | (30, 0,"オフィスチェア"), 22 | (31,30,"デスクチェア"), 23 | (32,30,"ビジネスチェア"), 24 | (33,30,"回転チェア"), 25 | (34,30,"リクライニングチェア"), 26 | (35,30,"投擲用椅子"), 27 | (40,0,"折りたたみ椅子"), 28 | (41,40,"パイプ椅子"), 29 | (42,40,"木製折りたたみ椅子"), 30 | (43,40,"キッチンチェア"), 31 | (44,40,"アウトドアチェア"), 32 | (45,40,"作業椅子"), 33 | (50, 0,"ベンチ"), 34 | (51,50,"一人掛けベンチ"), 35 | (52,50,"二人掛けベンチ"), 36 | (53,50,"アウトドア用ベンチ"), 37 | (54,50,"収納付きベンチ"), 38 | (55,50,"背もたれ付きベンチ"), 39 | (56,50,"ベンチマーク"), 40 | (60, 0,"座椅子"), 41 | (61,60,"和風座椅子"), 42 | (62,60,"高座椅子"), 43 | (63,60,"ゲーミング座椅子"), 44 | (64,60,"ロッキングチェア"), 45 | (65,60,"座布団"), 46 | (66,60,"空気椅子") 47 | ; 48 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/CategoryItemListPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import { fetchTimelineAction } from '../actions/fetchTimelineAction'; 4 | import CategoryItemListPage from '../pages/CategoryItemListPage'; 5 | 6 | const mapStateToProps = (state: AppState) => { 7 | return { 8 | items: state.timeline.items, 9 | hasNext: state.timeline.hasNext, 10 | categoryId: state.timeline.categoryId, 11 | categoryName: state.timeline.categoryName, 12 | errorType: state.error.errorType, 13 | loading: state.page.isTimelineLoading, 14 | }; 15 | }; 16 | const mapDispatchToProps = (dispatch: any) => ({ 17 | load: (categoryId: number) => { 18 | dispatch(fetchTimelineAction(undefined, undefined, categoryId)); 19 | }, 20 | loadMore: ( 21 | createdAt: number, 22 | itemId: number, 23 | categoryId: number, 24 | page: number, 25 | ) => { 26 | dispatch(fetchTimelineAction(createdAt, itemId, categoryId)); 27 | }, 28 | }); 29 | 30 | export default connect( 31 | mapStateToProps, 32 | mapDispatchToProps, 33 | )(CategoryItemListPage); 34 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Seller/Initial.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core/styles'; 3 | import { Typography } from '@material-ui/core'; 4 | import Button from '@material-ui/core/Button'; 5 | 6 | const useStyles = makeStyles((theme: Theme) => ({ 7 | button: { 8 | margin: theme.spacing(1), 9 | }, 10 | })); 11 | 12 | type Props = { 13 | itemId: number; 14 | postShipped: (itemId: number) => void; 15 | }; 16 | 17 | const Initial: React.FC = ({ itemId, postShipped }) => { 18 | const classes = useStyles(); 19 | 20 | const onClick = (e: React.MouseEvent) => { 21 | postShipped(itemId); 22 | }; 23 | 24 | return ( 25 | 26 | 商品が購入されました 27 | 28 | 下記の「集荷予約」を押し、集荷予約の手続きをしてください 29 | 30 | 38 | 39 | ); 40 | }; 41 | 42 | export default Initial; 43 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/timelineReducer.ts: -------------------------------------------------------------------------------- 1 | import { TimelineItem } from '../dataObjects/item'; 2 | import { FETCH_TIMELINE_SUCCESS } from '../actions/fetchTimelineAction'; 3 | import { PATH_NAME_CHANGE } from '../actions/locationChangeAction'; 4 | import { ActionTypes } from '../actions/actionTypes'; 5 | 6 | export interface TimelineState { 7 | items: TimelineItem[]; 8 | hasNext: boolean; 9 | categoryId?: number; 10 | categoryName?: string; 11 | } 12 | 13 | const initialState: TimelineState = { 14 | items: [], 15 | hasNext: false, 16 | }; 17 | 18 | const timeline = ( 19 | state: TimelineState = initialState, 20 | action: ActionTypes, 21 | ): TimelineState => { 22 | switch (action.type) { 23 | case PATH_NAME_CHANGE: 24 | return initialState; 25 | case FETCH_TIMELINE_SUCCESS: 26 | const { payload } = action; 27 | return { 28 | items: state.items.concat(payload.items), 29 | hasNext: payload.hasNext, 30 | categoryId: payload.categoryId, 31 | categoryName: payload.categoryName, 32 | }; 33 | default: 34 | return { ...state }; 35 | } 36 | }; 37 | 38 | export default timeline; 39 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Buyer/WaitDone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core/styles'; 3 | import { Typography } from '@material-ui/core'; 4 | import Button from '@material-ui/core/Button'; 5 | 6 | const useStyles = makeStyles((theme: Theme) => ({ 7 | button: { 8 | margin: theme.spacing(1), 9 | }, 10 | })); 11 | 12 | type Props = { 13 | itemId: number; 14 | postComplete: (itemId: number) => void; 15 | }; 16 | 17 | const WaitDone: React.FC = ({ itemId, postComplete }) => { 18 | const classes = useStyles(); 19 | 20 | const onClick = (e: React.MouseEvent) => { 21 | postComplete(itemId); 22 | }; 23 | 24 | return ( 25 | 26 | 商品が発送されました 27 | 28 | 商品が届き次第、下記の「取引完了」を押してください 29 | 30 | 38 | 39 | ); 40 | }; 41 | 42 | export default WaitDone; 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,vim 3 | # Edit at https://www.gitignore.io/?templates=linux,vim 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### Vim ### 21 | # Swap 22 | [._]*.s[a-v][a-z] 23 | [._]*.sw[a-p] 24 | [._]s[a-rt-v][a-z] 25 | [._]ss[a-gi-z] 26 | [._]sw[a-p] 27 | 28 | # Session 29 | Session.vim 30 | Sessionx.vim 31 | 32 | # Temporary 33 | .netrwhist 34 | # Auto-generated tag files 35 | tags 36 | # Persistent undo 37 | [._]*.un~ 38 | 39 | # End of https://www.gitignore.io/api/linux,vim 40 | 41 | 42 | /.*_history 43 | /.*_logout 44 | /.ssh 45 | /.gitconfig 46 | /logs 47 | /.viminfo 48 | /.sudo_as_admin_successful 49 | /.lesshst 50 | /.gem 51 | /.bundle 52 | /.cache 53 | 54 | *.log 55 | *.prof 56 | /xbuild 57 | /local 58 | /go 59 | /.npm 60 | /.cpanm 61 | /isucari/webapp/python/venv 62 | /.composer 63 | -------------------------------------------------------------------------------- /recipe.rb: -------------------------------------------------------------------------------- 1 | HOST = node[:hostname] 2 | USER = 'isucon' 3 | 4 | service 'isucari.golang.service' do 5 | action %i[enable start] 6 | end 7 | 8 | ### 9 | # Monitoring tools 10 | ### 11 | 12 | execute 'install netdata' do 13 | command 'bash -c "bash <(curl -Ss https://my-netdata.io/kickstart.sh) all --dont-wait"' 14 | not_if "systemctl list-unit-files | grep '^netdata.service'" 15 | end 16 | 17 | service 'netdata' do 18 | action %i[disable stop] 19 | end 20 | 21 | package 'percona-toolkit' 22 | 23 | ### 24 | # Configure middlewares 25 | ### 26 | 27 | #package 'redis' 28 | 29 | #service 'redis' do 30 | # action %i[enable start] 31 | #end 32 | 33 | if HOST == 'isu01' || HOST == 'isu02' 34 | service 'mysql' do 35 | action %i[disable stop] 36 | end 37 | end 38 | 39 | { 40 | 'nginx' => '/etc/nginx/nginx.conf', 41 | 'mysql' => '/etc/mysql/mysql.conf.d/mysqld.cnf', 42 | }.each do |service, conf| 43 | conf_source = "./#{HOST}/#{File.basename conf}" 44 | next unless File.file? conf_source 45 | 46 | service service do 47 | action %i[enable start] 48 | end 49 | 50 | remote_file conf do 51 | source conf_source 52 | mode '0644' 53 | notifies :restart, "service[#{service}]" 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /isu02/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | pid /run/nginx.pid; 4 | include /etc/nginx/modules-enabled/*.conf; 5 | worker_rlimit_nofile 100000; 6 | 7 | error_log /var/log/nginx/error.log error; 8 | 9 | events { 10 | worker_connections 4096; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | server_tokens off; 18 | sendfile on; 19 | tcp_nopush on; 20 | tcp_nodelay on; 21 | keepalive_timeout 120; 22 | client_max_body_size 10m; 23 | 24 | open_file_cache max=100 inactive=65s; 25 | gzip_static on; 26 | 27 | access_log off; 28 | 29 | # TLS configuration 30 | ssl_protocols TLSv1.2; 31 | ssl_prefer_server_ciphers on; 32 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; 33 | 34 | upstream app { 35 | server 127.0.0.1:8000; 36 | } 37 | 38 | server { 39 | listen 8080; 40 | 41 | location / { 42 | proxy_pass http://app; 43 | proxy_set_header Host $host; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /isu03/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | pid /run/nginx.pid; 4 | include /etc/nginx/modules-enabled/*.conf; 5 | worker_rlimit_nofile 100000; 6 | 7 | error_log /var/log/nginx/error.log error; 8 | 9 | events { 10 | worker_connections 4096; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | server_tokens off; 18 | sendfile on; 19 | tcp_nopush on; 20 | tcp_nodelay on; 21 | keepalive_timeout 120; 22 | client_max_body_size 10m; 23 | 24 | open_file_cache max=100 inactive=65s; 25 | gzip_static on; 26 | 27 | access_log off; 28 | 29 | # TLS configuration 30 | ssl_protocols TLSv1.2; 31 | ssl_prefer_server_ciphers on; 32 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; 33 | 34 | upstream app { 35 | server 127.0.0.1:8000; 36 | } 37 | 38 | server { 39 | listen 8080; 40 | 41 | location / { 42 | proxy_pass http://app; 43 | proxy_set_header Host $host; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /isucari/webapp/php/public/index.php: -------------------------------------------------------------------------------- 1 | run(); 39 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/SellFormContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SellFormComponent from '../components/SellFormComponent'; 3 | import { listItemAction } from '../actions/sellingItemAction'; 4 | import { AppState } from '../index'; 5 | import { AnyAction } from 'redux'; 6 | import { ThunkDispatch } from 'redux-thunk'; 7 | import { CategorySimple } from '../dataObjects/category'; 8 | 9 | const mapStateToProps = (state: AppState) => { 10 | // Note: Parent category's parent_id is 0 11 | const categories = state.categories.categories.filter( 12 | (category: CategorySimple) => category.parentId !== 0, 13 | ); 14 | 15 | return { 16 | error: state.formError.error, 17 | categories, 18 | }; 19 | }; 20 | const mapDispatchToProps = ( 21 | dispatch: ThunkDispatch, 22 | ) => ({ 23 | sellItem: ( 24 | name: string, 25 | description: string, 26 | price: number, 27 | categoryId: number, 28 | image: Blob, 29 | ) => { 30 | dispatch(listItemAction(name, description, price, categoryId, image)); 31 | }, 32 | }); 33 | 34 | export default connect( 35 | mapStateToProps, 36 | mapDispatchToProps, 37 | )(SellFormComponent); 38 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/dataObjects/item.ts: -------------------------------------------------------------------------------- 1 | import { UserData } from './user'; 2 | import { Category } from './category'; 3 | import { TransactionStatus } from './transaction'; 4 | import { ShippingStatus } from './shipping'; 5 | 6 | export interface ItemData { 7 | id: number; 8 | sellerId: number; 9 | seller: UserData; 10 | buyerId?: number; 11 | buyer?: UserData; 12 | status: ItemStatus; 13 | name: string; 14 | price: number; 15 | description: string; 16 | thumbnailUrl: string; 17 | category: Category; 18 | transactionEvidenceId?: number; 19 | transactionEvidenceStatus?: TransactionStatus; 20 | shippingStatus?: ShippingStatus; 21 | createdAt: number; 22 | } 23 | 24 | export type TimelineItem = { 25 | id: number; 26 | status: ItemStatus; 27 | name: string; 28 | price: number; 29 | thumbnailUrl: string; 30 | createdAt: number; 31 | }; 32 | 33 | export type TransactionItem = { 34 | id: number; 35 | status: ItemStatus; 36 | transactionEvidenceStatus: TransactionStatus; 37 | shippingStatus: ShippingStatus; 38 | name: string; 39 | thumbnailUrl: string; 40 | createdAt: number; 41 | }; 42 | 43 | export type ItemStatus = 'on_sale' | 'trading' | 'sold_out' | 'stop' | 'cancel'; 44 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionComponent/TransactionComponent.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { TransactionComponent } from '.'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | import { TransactionItem } from '../../dataObjects/item'; 6 | 7 | const stories = storiesOf('components/TransactionComponent', module); 8 | 9 | const item: TransactionItem = { 10 | id: 1, 11 | status: 'trading', 12 | transactionEvidenceStatus: 'wait_shipping', 13 | shippingStatus: 'initial', 14 | name: 'テスト商品', 15 | thumbnailUrl: 'https://i.gyazo.com/8560fce19556b64c95ad091350910184.jpg', 16 | createdAt: 111111111, 17 | }; 18 | const onClick = (item: TransactionItem) => {}; 19 | 20 | stories 21 | .add('default', () => ( 22 | 23 | 24 | 25 | )) 26 | .add('long name', () => ( 27 | 28 | 35 | 36 | )); 37 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionBuyer/TransactionBuyer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TransactionStatus } from '../../dataObjects/transaction'; 3 | import { ShippingStatus } from '../../dataObjects/shipping'; 4 | import Initial from '../Transaction/Buyer/Initial'; 5 | import WaitShipping from '../Transaction/Buyer/WaitShipping'; 6 | import WaitDone from '../Transaction/Buyer/WaitDone'; 7 | import Done from '../Transaction/Buyer/Done'; 8 | 9 | export type Props = { 10 | itemId: number; 11 | postComplete: (itemId: number) => void; 12 | transactionStatus: TransactionStatus; 13 | shippingStatus: ShippingStatus; 14 | }; 15 | 16 | const TransactionBuyer: React.FC = ({ 17 | itemId, 18 | postComplete, 19 | transactionStatus, 20 | shippingStatus, 21 | }) => { 22 | if (shippingStatus === 'initial' && transactionStatus === 'wait_shipping') { 23 | return ; 24 | } 25 | 26 | if ( 27 | shippingStatus === 'wait_pickup' && 28 | transactionStatus === 'wait_shipping' 29 | ) { 30 | return ; 31 | } 32 | 33 | if (transactionStatus === 'wait_done') { 34 | return ; 35 | } 36 | 37 | return ; 38 | }; 39 | 40 | export { TransactionBuyer }; 41 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/userItemsReducer.ts: -------------------------------------------------------------------------------- 1 | import { TimelineItem } from '../dataObjects/item'; 2 | import { FETCH_USER_ITEMS_SUCCESS } from '../actions/fetchUserItemsAction'; 3 | import { FETCH_USER_PAGE_DATA_SUCCESS } from '../actions/fetchUserPageDataAction'; 4 | import { ActionTypes } from '../actions/actionTypes'; 5 | import { PATH_NAME_CHANGE } from '../actions/locationChangeAction'; 6 | 7 | export interface UserItemsState { 8 | items: TimelineItem[]; 9 | hasNext: boolean; 10 | } 11 | 12 | const initialState: UserItemsState = { 13 | items: [], 14 | hasNext: false, 15 | }; 16 | 17 | const userItems = ( 18 | state: UserItemsState = initialState, 19 | action: ActionTypes, 20 | ): UserItemsState => { 21 | switch (action.type) { 22 | case PATH_NAME_CHANGE: 23 | // MEMO: ページ遷移したら再度APIを叩かせるようにリセットする 24 | return initialState; 25 | case FETCH_USER_ITEMS_SUCCESS: 26 | return { 27 | items: state.items.concat(action.payload.items), 28 | hasNext: action.payload.hasNext, 29 | }; 30 | case FETCH_USER_PAGE_DATA_SUCCESS: 31 | return { 32 | items: state.items.concat(action.payload.items), 33 | hasNext: action.payload.itemsHasNext, 34 | }; 35 | default: 36 | return { ...state }; 37 | } 38 | }; 39 | 40 | export default userItems; 41 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Route/AuthRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect, Route, RouteProps } from 'react-router'; 3 | import { routes } from '../../routes/Route'; 4 | import LoadingComponent from '../LoadingComponent'; 5 | import { InternalServerErrorPage } from '../../pages/error/InternalServerErrorPage'; 6 | 7 | type Props = { 8 | isLoggedIn: boolean; 9 | loading: boolean; 10 | load: () => void; 11 | alreadyLoaded: boolean; 12 | error?: string; 13 | } & RouteProps; 14 | 15 | const AuthRoute: React.FC = ({ 16 | component: Component, 17 | isLoggedIn, 18 | loading, 19 | load, 20 | alreadyLoaded, 21 | error, 22 | ...rest 23 | }) => { 24 | if (!Component) { 25 | throw new Error('component attribute required for AuthRoute component'); 26 | } 27 | 28 | if (error) { 29 | return ; 30 | } 31 | 32 | if (!alreadyLoaded) { 33 | load(); 34 | } 35 | 36 | return ( 37 | 40 | loading ? ( 41 | 42 | ) : isLoggedIn ? ( 43 | 44 | ) : ( 45 | 46 | ) 47 | } 48 | /> 49 | ); 50 | }; 51 | 52 | export { AuthRoute }; 53 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/ItemPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ItemPage from '../pages/ItemPage'; 3 | import { fetchItemAction } from '../actions/fetchItemAction'; 4 | import { AppState } from '../index'; 5 | import { push } from 'connected-react-router'; 6 | import { routes } from '../routes/Route'; 7 | import { postBumpAction } from '../actions/postBumpAction'; 8 | 9 | const mapStateToProps = (state: AppState) => ({ 10 | loading: state.page.isItemLoading, 11 | item: state.viewingItem.item, 12 | viewer: { 13 | userId: state.authStatus.userId || 0, 14 | }, 15 | errorType: state.error.errorType, 16 | }); 17 | const mapDispatchToProps = (dispatch: any) => ({ 18 | load: (itemId: string) => { 19 | dispatch(fetchItemAction(itemId)); 20 | }, 21 | onClickBuy: (itemId: number) => { 22 | dispatch(push(routes.buy.getPath(itemId))); 23 | }, 24 | onClickItemEdit: (itemId: number) => { 25 | dispatch(push(routes.itemEdit.getPath(itemId))); 26 | }, 27 | onClickBump: (itemId: number) => { 28 | dispatch(postBumpAction(itemId)); 29 | }, 30 | onClickTransaction: (itemId: number) => { 31 | dispatch(push(routes.transaction.getPath(itemId))); 32 | }, 33 | }); 34 | 35 | export default connect( 36 | mapStateToProps, 37 | mapDispatchToProps, 38 | )(ItemPage); 39 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/formErrorReducer.ts: -------------------------------------------------------------------------------- 1 | import { SELLING_ITEM_FAIL } from '../actions/sellingItemAction'; 2 | import { BUY_FAIL, USING_CARD_FAIL } from '../actions/buyAction'; 3 | import { POST_ITEM_EDIT_FAIL } from '../actions/postItemEditAction'; 4 | import { ActionTypes } from '../actions/actionTypes'; 5 | 6 | export interface FormErrorState { 7 | error?: string; 8 | buyFormError?: BuyFormErrorState; 9 | itemEditFormError?: string; 10 | } 11 | 12 | export interface BuyFormErrorState { 13 | cardError?: string; // Error from payment service 14 | buyError?: string; // Error from app 15 | } 16 | 17 | const initialState: FormErrorState = { 18 | error: undefined, 19 | buyFormError: {}, 20 | itemEditFormError: undefined, 21 | }; 22 | 23 | const formError = ( 24 | state: FormErrorState = initialState, 25 | action: ActionTypes, 26 | ): FormErrorState => { 27 | switch (action.type) { 28 | case USING_CARD_FAIL: 29 | case BUY_FAIL: 30 | case SELLING_ITEM_FAIL: { 31 | return { 32 | ...action.payload, 33 | }; 34 | } 35 | case POST_ITEM_EDIT_FAIL: 36 | return { 37 | ...state, 38 | itemEditFormError: action.payload.itemEditFormError, 39 | }; 40 | default: 41 | return initialState; 42 | } 43 | }; 44 | 45 | export default formError; 46 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemList/ItemList.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { ItemList, Props } from '.'; 4 | import { ItemStatus, TimelineItem } from '../../dataObjects/item'; 5 | import { MemoryRouter } from 'react-router-dom'; 6 | 7 | const stories = storiesOf('components/ItemList', module); 8 | 9 | const getMockItem = ( 10 | id: number, 11 | status: ItemStatus, 12 | name: string, 13 | price: number, 14 | ): TimelineItem => ({ 15 | id, 16 | status, 17 | name, 18 | price, 19 | thumbnailUrl: 'https://i.gyazo.com/8560fce19556b64c95ad091350910184.jpg', 20 | createdAt: 11111, 21 | }); 22 | 23 | const mockProps: Props = { 24 | items: [], 25 | hasNext: false, 26 | loadMore: page => {}, 27 | }; 28 | 29 | stories 30 | .add('no items', () => ( 31 | 32 | 33 | 34 | )) 35 | .add('items', () => { 36 | const mockItems: TimelineItem[] = []; 37 | for (let i = 1; i <= 30; i++) { 38 | mockItems.push( 39 | getMockItem(i, i % 2 ? 'on_sale' : 'sold_out', `test ${i}`, i * 1000), 40 | ); 41 | } 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/ItemBuyPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ItemBuyFormContainer from '../containers/ItemBuyFormContainer'; 3 | import BasePageContainer from '../containers/BasePageContainer'; 4 | import { RouteComponentProps } from 'react-router-dom'; 5 | import { ItemData } from '../dataObjects/item'; 6 | import LoadingComponent from '../components/LoadingComponent'; 7 | import { ErrorProps, PageComponentWithError } from '../hoc/withBaseComponent'; 8 | 9 | type Props = { 10 | loading: boolean; 11 | load: (itemId: string) => void; 12 | item?: ItemData; 13 | } & RouteComponentProps<{ item_id: string }> & 14 | ErrorProps; 15 | 16 | class ItemBuyPage extends React.Component { 17 | constructor(props: Props) { 18 | super(props); 19 | 20 | const { item } = props; 21 | const item_id = props.match.params.item_id; 22 | 23 | // 商品が渡されない or 渡された商品とURLが一致しない場合は商品取得をする 24 | if (!item || item.id.toString() !== item_id) { 25 | props.load(item_id); 26 | } 27 | } 28 | 29 | render() { 30 | const { loading } = this.props; 31 | 32 | return ( 33 | 34 | {loading ? : } 35 | 36 | ); 37 | } 38 | } 39 | 40 | export default PageComponentWithError()(ItemBuyPage); 41 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Route/NonAuthRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect, Route, RouteProps } from 'react-router'; 3 | import { routes } from '../../routes/Route'; 4 | import LoadingComponent from '../LoadingComponent'; 5 | import { InternalServerErrorPage } from '../../pages/error/InternalServerErrorPage'; 6 | 7 | type Props = { 8 | isLoggedIn: boolean; 9 | loading: boolean; 10 | load: () => void; 11 | alreadyLoaded: boolean; 12 | error?: string; 13 | } & RouteProps; 14 | 15 | const NonAuthRoute: React.FC = ({ 16 | component: Component, 17 | isLoggedIn, 18 | loading, 19 | load, 20 | alreadyLoaded, 21 | error, 22 | ...rest 23 | }) => { 24 | if (!Component) { 25 | throw new Error('component attribute required for NonAuthRoute component'); 26 | } 27 | 28 | if (error) { 29 | return ; 30 | } 31 | 32 | if (!alreadyLoaded) { 33 | load(); 34 | } 35 | 36 | return ( 37 | 40 | loading ? ( 41 | 42 | ) : !isLoggedIn ? ( 43 | 44 | ) : ( 45 | 46 | ) 47 | } 48 | /> 49 | ); 50 | }; 51 | 52 | export { NonAuthRoute }; 53 | -------------------------------------------------------------------------------- /isucari/webapp/php/src/settings.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'displayErrorDetails' => true, // set to false in production 8 | 'addContentLengthHeader' => false, // Allow the web server to send the content-length header 9 | 'determineRouteBeforeAppMiddleware' => true, 10 | 11 | // Renderer settings 12 | 'renderer' => [ 13 | 'template_path' => __DIR__ . '/../../public/', 14 | ], 15 | 16 | // Monolog settings 17 | 'logger' => [ 18 | 'name' => 'isucari', 19 | // 'path' => __DIR__ . '/../logs/app.log', 20 | 'path' => 'php://stdout', 21 | 'level' => \Monolog\Logger::INFO, 22 | ], 23 | 24 | // Database settings 25 | 'database' => [ 26 | 'host' => Environment::get('MYSQL_HOST', '127.0.0.1'), 27 | 'port' => Environment::get('MYSQL_PORT', '3306'), 28 | 'username' => Environment::get('MYSQL_USER', 'isucari'), 29 | 'password' => Environment::get('MYSQL_PASS', 'isucari'), 30 | 'dbname' => Environment::get('MYSQL_DBNAME', 'isucari'), 31 | ], 32 | 'app' => [ 33 | 'base_dir' => __DIR__ . '/../', 34 | 'upload_path' => __DIR__ . '/../../public/upload/', 35 | ], 36 | ], 37 | ]; 38 | -------------------------------------------------------------------------------- /isucari/webapp/public/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.b2bd30b977e2fb5edb9ffe534b18d478.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^\/]+\.[^\/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/transactionsReducer.ts: -------------------------------------------------------------------------------- 1 | import { TransactionItem } from '../dataObjects/item'; 2 | import { FETCH_TRANSACTIONS_SUCCESS } from '../actions/fetchTransactionsAction'; 3 | import { FETCH_USER_PAGE_DATA_SUCCESS } from '../actions/fetchUserPageDataAction'; 4 | import { ActionTypes } from '../actions/actionTypes'; 5 | import { PATH_NAME_CHANGE } from '../actions/locationChangeAction'; 6 | 7 | export interface TransactionsState { 8 | items: TransactionItem[]; 9 | hasNext: boolean; 10 | } 11 | 12 | const initialState: TransactionsState = { 13 | items: [], 14 | hasNext: false, 15 | }; 16 | 17 | const transactions = ( 18 | state: TransactionsState = initialState, 19 | action: ActionTypes, 20 | ): TransactionsState => { 21 | switch (action.type) { 22 | case PATH_NAME_CHANGE: 23 | // MEMO: ページ遷移したら再度APIを叩かせるようにリセットする 24 | return initialState; 25 | case FETCH_TRANSACTIONS_SUCCESS: 26 | return { 27 | items: state.items.concat(action.payload.items), 28 | hasNext: action.payload.hasNext, 29 | }; 30 | case FETCH_USER_PAGE_DATA_SUCCESS: 31 | return { 32 | items: state.items.concat(action.payload.transactions), 33 | hasNext: action.payload.transactionsHasNext, 34 | }; 35 | default: 36 | return { ...state }; 37 | } 38 | }; 39 | 40 | export default transactions; 41 | -------------------------------------------------------------------------------- /isucari/webapp/go/categories.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | var allCategories []Category 11 | var categoryByID map[int]Category 12 | var childCategories map[int][]int 13 | 14 | func loadCategories() { 15 | err := dbx.Select(&allCategories, "SELECT * FROM categories") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | categoryByID = make(map[int]Category, len(allCategories)) 21 | childCategories = make(map[int][]int) 22 | for _, c := range allCategories { 23 | categoryByID[c.ID] = c 24 | } 25 | 26 | for _, c := range categoryByID { 27 | if c.ParentID > 0 { 28 | c.ParentCategoryName = categoryByID[c.ParentID].CategoryName 29 | } 30 | categoryByID[c.ID] = c 31 | 32 | var children []int 33 | for _, child := range categoryByID { 34 | if child.ParentID == c.ID { 35 | children = append(children, child.ID) 36 | } 37 | } 38 | if len(children) > 0 { 39 | childCategories[c.ID] = children 40 | } 41 | } 42 | } 43 | 44 | func getCategoryByID(_ sqlx.Queryer, id int) (Category, error) { 45 | c, ok := categoryByID[id] 46 | if !ok { 47 | return Category{}, sql.ErrNoRows 48 | } 49 | return c, nil 50 | } 51 | 52 | func getChildCategories(parent int) []int { 53 | return childCategories[parent] 54 | } 55 | 56 | func getAllCategories() []Category { 57 | return allCategories 58 | } 59 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionList/TransactionList.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { TransactionList } from '.'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | import { TransactionItem } from '../../dataObjects/item'; 6 | 7 | const stories = storiesOf('components/TransactionList', module); 8 | 9 | const item: TransactionItem = { 10 | id: 1, 11 | status: 'trading', 12 | transactionEvidenceStatus: 'wait_shipping', 13 | shippingStatus: 'initial', 14 | name: 'テスト商品', 15 | thumbnailUrl: 'https://i.gyazo.com/8560fce19556b64c95ad091350910184.jpg', 16 | createdAt: 111111111, 17 | }; 18 | 19 | const mockProps = { 20 | items: [item], 21 | hasNext: false, 22 | loadMore: (createdAt: number, itemId: number, page: number) => {}, 23 | }; 24 | 25 | /** 26 | stories 27 | .add('default', () => ( 28 | 29 | 30 | 31 | )) 32 | .add('many transactions', () => ( 33 | 34 | 39 | Object.assign({}, item, { id: item.id + index, onClickTransaction: (item: TransactionItem) => {}}), 40 | )} 41 | /> 42 | 43 | )); 44 | */ 45 | -------------------------------------------------------------------------------- /isucari/webapp/public/static/css/main.19393e92.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css","App.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mIAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,KACE,uEAEF,CCZA,KACE,iBACF,CAEA,UACE,mDAA4C,CAA5C,2CAA4C,CAC5C,aAAc,CACd,mBACF,CAEA,YACE,wBAAyB,CACzB,gBAAiB,CACjB,YAAa,CACb,qBAAsB,CACtB,kBAAmB,CACnB,sBAAuB,CACvB,4BAA6B,CAC7B,UACF,CAEA,UACE,aACF,CAEA,iCACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAPA,yBACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF","file":"main.19393e92.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n",".App {\n text-align: center;\n}\n\n.App-logo {\n animation: App-logo-spin infinite 20s linear;\n height: 40vmin;\n pointer-events: none;\n}\n\n.App-header {\n background-color: #282c34;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n"]} -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/HeaderContainer.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from '../index'; 2 | import { Dispatch } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { push } from 'connected-react-router'; 5 | import { routes } from '../routes/Route'; 6 | import { Header } from '../components/Header'; 7 | import { CategorySimple } from '../dataObjects/category'; 8 | 9 | const mapStateToProps = (state: AppState) => ({ 10 | isLoggedIn: !!state.authStatus.userId, 11 | ownUserId: state.authStatus.userId || 0, 12 | // Note: Showing only parent category 13 | categories: state.categories.categories.filter( 14 | (category: CategorySimple) => category.parentId === 0, 15 | ), 16 | }); 17 | 18 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 19 | goToTopPage: () => { 20 | dispatch(push(routes.timeline.path)); 21 | }, 22 | goToUserPage: (userId: number) => { 23 | dispatch(push(routes.user.getPath(userId))); 24 | }, 25 | goToSettingPage: () => { 26 | dispatch(push(routes.userSetting.path)); 27 | }, 28 | goToCategoryItemList: (categoryId: number) => { 29 | dispatch(push(routes.categoryTimeline.getPath(categoryId))); 30 | }, 31 | onClickTitle: (isLoggedIn: boolean) => { 32 | if (isLoggedIn) { 33 | dispatch(push(routes.timeline.path)); 34 | return; 35 | } 36 | dispatch(push(routes.top.path)); 37 | }, 38 | }); 39 | 40 | export default connect( 41 | mapStateToProps, 42 | mapDispatchToProps, 43 | )(Header); 44 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Item/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from '@material-ui/core/Card'; 3 | import { Link as RouteLink } from 'react-router-dom'; 4 | import GridListTileBar from '@material-ui/core/GridListTileBar'; 5 | import makeStyles from '@material-ui/core/styles/makeStyles'; 6 | import { routes } from '../../routes/Route'; 7 | import { Theme } from '@material-ui/core'; 8 | import { ItemStatus } from '../../dataObjects/item'; 9 | import { ItemImage } from '../ItemImage'; 10 | import GridListTile from '@material-ui/core/GridListTile'; 11 | 12 | const useStyles = makeStyles((theme: Theme) => ({ 13 | card: { 14 | width: '300px', 15 | position: 'relative', 16 | }, 17 | })); 18 | 19 | interface Props { 20 | itemId: number; 21 | imageUrl: string; 22 | title: string; 23 | price: number; 24 | status: ItemStatus; 25 | } 26 | 27 | const Item: React.FC = ({ itemId, imageUrl, title, price, status }) => { 28 | const classes = useStyles(); 29 | 30 | return ( 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export { Item }; 48 | -------------------------------------------------------------------------------- /isucari/webapp/public/static/js/runtime~main.a8a9905a.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c 7 | createStyles({ 8 | root: { 9 | position: 'relative', 10 | }, 11 | button: { 12 | margin: theme.spacing(1), 13 | }, 14 | }); 15 | 16 | export interface Props extends WithStyles { 17 | onClick: (e: React.MouseEvent) => void; 18 | buttonName: string; 19 | loading: boolean; 20 | } 21 | 22 | class BaseLoadingButton extends React.Component { 23 | constructor(props: Props) { 24 | super(props); 25 | 26 | this._onClick = this._onClick.bind(this); 27 | } 28 | 29 | _onClick(e: React.MouseEvent) { 30 | e.preventDefault(); 31 | 32 | this.props.onClick(e); 33 | } 34 | 35 | render() { 36 | const { loading, buttonName, classes } = this.props; 37 | 38 | return ( 39 |
40 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | export const LoadingButton = withStyles(styles)(BaseLoadingButton); 55 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemFooter/ItemFooter.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { ItemFooter } from '.'; 4 | import { Typography } from '@material-ui/core'; 5 | import { ReactElement } from 'react'; 6 | 7 | const stories = storiesOf('components/ItemFooter', module); 8 | 9 | const getMockButton = ( 10 | text: string, 11 | disabled: boolean, 12 | tooltip?: ReactElement, 13 | ) => ({ 14 | onClick: (e: React.MouseEvent) => {}, 15 | buttonText: text, 16 | disabled, 17 | tooltip, 18 | }); 19 | 20 | const mockProps = { 21 | price: 1000, 22 | buttons: [getMockButton('購入', false)], 23 | }; 24 | 25 | stories 26 | .add('single button', () => ) 27 | .add('multiple button', () => ( 28 | 32 | )) 33 | .add('disabled button', () => ( 34 | 35 | )) 36 | .add('with tooltip', () => ( 37 | 44 | 新機能! 45 | 46 | BUMPを使って商品をタイムラインの一番上に押し上げよう!' 47 | 48 | , 49 | ), 50 | ]} 51 | /> 52 | )); 53 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionSeller/TransactionSeller.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TransactionStatus } from '../../dataObjects/transaction'; 3 | import { ShippingStatus } from '../../dataObjects/shipping'; 4 | import Initial from '../Transaction/Seller/Initial'; 5 | import WaitShipping from '../Transaction/Seller/WaitShipping'; 6 | import WaitDone from '../Transaction/Seller/WaitDone'; 7 | import Done from '../Transaction/Seller/Done'; 8 | 9 | export type Props = { 10 | itemId: number; 11 | transactionEvidenceId: number; 12 | postShipped: (itemId: number) => void; 13 | postShippedDone: (itemId: number) => void; 14 | transactionStatus: TransactionStatus; 15 | shippingStatus: ShippingStatus; 16 | }; 17 | 18 | const TransactionSeller: React.FC = ({ 19 | itemId, 20 | transactionEvidenceId, 21 | postShipped, 22 | postShippedDone, 23 | transactionStatus, 24 | shippingStatus, 25 | }) => { 26 | if (shippingStatus === 'initial' && transactionStatus === 'wait_shipping') { 27 | return ; 28 | } 29 | 30 | if ( 31 | shippingStatus === 'wait_pickup' && 32 | transactionStatus === 'wait_shipping' 33 | ) { 34 | return ( 35 | 40 | ); 41 | } 42 | 43 | if (transactionStatus === 'wait_done') { 44 | return ; 45 | } 46 | 47 | return ; 48 | }; 49 | 50 | export { TransactionSeller }; 51 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/BasePageComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { 3 | Container, 4 | MuiThemeProvider, 5 | Theme, 6 | WithStyles, 7 | } from '@material-ui/core'; 8 | import LoadingComponent from './LoadingComponent'; 9 | import HeaderContainer from '../containers/HeaderContainer'; 10 | import SnackBarContainer from '../containers/SnackBarContainer'; 11 | import { StyleRules } from '@material-ui/core/styles'; 12 | import createStyles from '@material-ui/core/styles/createStyles'; 13 | import withStyles from '@material-ui/core/styles/withStyles'; 14 | import { themeInstance } from '../theme'; 15 | 16 | const styles = (theme: Theme): StyleRules => 17 | createStyles({ 18 | container: { 19 | paddingTop: theme.spacing(12), 20 | }, 21 | }); 22 | 23 | interface BaseProps extends WithStyles { 24 | loading: boolean; 25 | } 26 | 27 | export type Props = PropsWithChildren; 28 | 29 | class BasePageComponent extends React.Component { 30 | render() { 31 | const { classes } = this.props; 32 | 33 | return ( 34 | 35 | 36 | 37 | {this.props.loading ? ( 38 | 39 | ) : ( 40 | this.props.children || null 41 | )} 42 | 43 | 44 | 45 | ); 46 | } 47 | } 48 | 49 | export default withStyles(styles)(BasePageComponent); 50 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/containers/UserPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState } from '../index'; 3 | import UserPage from '../pages/UserPage'; 4 | import { ThunkDispatch } from 'redux-thunk'; 5 | import { AnyAction } from 'redux'; 6 | import { fetchTransactionsAction } from '../actions/fetchTransactionsAction'; 7 | import { fetchUserItemsAction } from '../actions/fetchUserItemsAction'; 8 | import { fetchUserPageDataAction } from '../actions/fetchUserPageDataAction'; 9 | 10 | const mapStateToProps = (state: AppState) => ({ 11 | loading: state.page.isUserPageLoading, 12 | loggedInUserId: state.authStatus.userId, 13 | items: state.userItems.items, 14 | itemsHasNext: state.userItems.hasNext, 15 | transactions: state.transactions.items, 16 | transactionsHasNext: state.transactions.hasNext, 17 | user: state.viewingUser.user, 18 | errorType: state.error.errorType, 19 | }); 20 | const mapDispatchToProps = ( 21 | dispatch: ThunkDispatch, 22 | ) => ({ 23 | load: (userId: number, isMyPage: boolean) => { 24 | dispatch(fetchUserPageDataAction(userId, isMyPage)); 25 | }, 26 | itemsLoadMore: ( 27 | userId: number, 28 | itemId: number, 29 | createdAt: number, 30 | page: number, 31 | ) => { 32 | dispatch(fetchUserItemsAction(userId, itemId, createdAt)); 33 | }, 34 | transactionsLoadMore: (itemId: number, createdAt: number, page: number) => { 35 | dispatch(fetchTransactionsAction(itemId, createdAt)); 36 | }, 37 | }); 38 | 39 | export default connect( 40 | mapStateToProps, 41 | mapDispatchToProps, 42 | )(UserPage); 43 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/snackBarReducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from '../actions/actionTypes'; 2 | import { POST_SHIPPED_FAIL } from '../actions/postShippedAction'; 3 | import { POST_SHIPPED_DONE_FAIL } from '../actions/postShippedDoneAction'; 4 | import { POST_COMPLETE_FAIL } from '../actions/postCompleteAction'; 5 | import { SNACK_BAR_CLOSE } from '../actions/snackBarAction'; 6 | import { POST_BUMP_FAIL, POST_BUMP_SUCCESS } from '../actions/postBumpAction'; 7 | import { SnackBarVariant } from '../components/SnackBar'; 8 | import { LOGIN_FAIL } from '../actions/authenticationActions'; 9 | import { REGISTER_FAIL } from '../actions/registerAction'; 10 | import { PATH_NAME_CHANGE } from '../actions/locationChangeAction'; 11 | 12 | export interface SnackBarState { 13 | reason: string; 14 | available: boolean; 15 | variant: SnackBarVariant; 16 | } 17 | 18 | const initialState: SnackBarState = { 19 | reason: '', 20 | available: false, 21 | variant: 'success', 22 | }; 23 | 24 | const snackBar = ( 25 | state: SnackBarState = initialState, 26 | action: ActionTypes, 27 | ): SnackBarState => { 28 | switch (action.type) { 29 | case LOGIN_FAIL: 30 | case REGISTER_FAIL: 31 | case POST_SHIPPED_FAIL: 32 | case POST_SHIPPED_DONE_FAIL: 33 | case POST_BUMP_SUCCESS: 34 | case POST_BUMP_FAIL: 35 | case POST_COMPLETE_FAIL: 36 | return { 37 | reason: action.snackBarMessage, 38 | available: true, 39 | variant: action.variant, 40 | }; 41 | case SNACK_BAR_CLOSE: 42 | case PATH_NAME_CHANGE: 43 | return initialState; 44 | default: 45 | return { ...state }; 46 | } 47 | }; 48 | 49 | export default snackBar; 50 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/error/NotFoundPage/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import makeStyles from '@material-ui/core/styles/makeStyles'; 3 | import { MuiThemeProvider, Theme } from '@material-ui/core'; 4 | import { themeInstance } from '../../../theme'; 5 | import Container from '@material-ui/core/Container'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import { Link } from 'react-router-dom'; 8 | import { routes } from '../../../routes/Route'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | container: { 12 | paddingTop: theme.spacing(2), 13 | display: 'flex', 14 | flexDirection: 'column', 15 | alignItems: 'center', 16 | }, 17 | img: { 18 | width: '70%', 19 | }, 20 | message: { 21 | paddingTop: theme.spacing(1), 22 | }, 23 | link: { 24 | paddingTop: theme.spacing(2), 25 | }, 26 | })); 27 | 28 | export type Props = { 29 | message?: string; 30 | }; 31 | 32 | const NotFoundPage: React.FC = ({ message }) => { 33 | const classes = useStyles(); 34 | 35 | return ( 36 | 37 | 38 | {'not 39 | 404 Not Found 40 | {message && ( 41 | 42 | {message} 43 | 44 | )} 45 | 46 | 47 | トップページへ 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export { NotFoundPage }; 56 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/Transaction/Seller/WaitShipping.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core/styles'; 3 | import { Typography } from '@material-ui/core'; 4 | import Button from '@material-ui/core/Button'; 5 | import Grid from '@material-ui/core/Grid'; 6 | 7 | const useStyles = makeStyles((theme: Theme) => ({ 8 | qrCode: { 9 | width: '300px', 10 | height: '300px', 11 | margin: theme.spacing(1), 12 | }, 13 | button: { 14 | margin: theme.spacing(1), 15 | }, 16 | })); 17 | 18 | type Props = { 19 | itemId: number; 20 | transactionEvidenceId: number; 21 | postShippedDone: (itemId: number) => void; 22 | }; 23 | 24 | const WaitShipping: React.FC = ({ 25 | itemId, 26 | transactionEvidenceId, 27 | postShippedDone, 28 | }) => { 29 | const classes = useStyles(); 30 | 31 | const qrCodeUrl = `/transactions/${transactionEvidenceId}.png`; 32 | const onClick = (e: React.MouseEvent) => { 33 | postShippedDone(itemId); 34 | }; 35 | 36 | return ( 37 | 38 | 39 | 集荷予約が完了しました 40 | 41 | 配達員に下記QRコードをお見せください 42 | 43 | 44 | 配達員に商品を渡したら下記の「発送完了」を押してください 45 | 46 | 47 | 48 | QRコード 49 | 50 | 51 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default WaitShipping; 65 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemList/ItemList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TimelineItem } from '../../dataObjects/item'; 3 | import makeStyles from '@material-ui/core/styles/makeStyles'; 4 | import GridList from '@material-ui/core/GridList'; 5 | import { Item } from '../Item'; 6 | import GridListTile from '@material-ui/core/GridListTile'; 7 | import InfiniteScroll from 'react-infinite-scroller'; 8 | import { Theme } from '@material-ui/core'; 9 | import { TimelineLoading } from '../TimelineLoading'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | gridList: { 13 | display: 'flex', 14 | flexWrap: 'wrap', 15 | justifyContent: 'flex-start', 16 | }, 17 | grid: { 18 | height: '100%', 19 | }, 20 | })); 21 | 22 | export interface Props { 23 | items: TimelineItem[]; 24 | hasNext: boolean; 25 | loadMore: (page: number) => void; 26 | } 27 | 28 | const ItemList: React.FC = function({ 29 | items, 30 | hasNext, 31 | loadMore, 32 | }: Props) { 33 | const classes = useStyles(); 34 | 35 | const itemComponents = []; 36 | 37 | for (const item of items) { 38 | itemComponents.push( 39 | 40 | 47 | , 48 | ); 49 | } 50 | 51 | return ( 52 | } 57 | > 58 | 59 | {itemComponents} 60 | 61 | 62 | ); 63 | }; 64 | 65 | export { ItemList }; 66 | -------------------------------------------------------------------------------- /isu01/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | pid /run/nginx.pid; 4 | include /etc/nginx/modules-enabled/*.conf; 5 | worker_rlimit_nofile 100000; 6 | 7 | error_log /var/log/nginx/error.log error; 8 | 9 | events { 10 | worker_connections 4096; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | server_tokens off; 18 | sendfile on; 19 | tcp_nopush on; 20 | tcp_nodelay on; 21 | keepalive_timeout 120; 22 | client_max_body_size 10m; 23 | 24 | open_file_cache max=100 inactive=65s; 25 | gzip_static on; 26 | 27 | access_log off; 28 | 29 | # TLS configuration 30 | ssl_protocols TLSv1.2; 31 | ssl_prefer_server_ciphers on; 32 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; 33 | 34 | upstream app { 35 | server 127.0.0.1:8000; 36 | } 37 | upstream login_app { 38 | server 127.0.0.1:8000 weight=2; 39 | server 172.24.83.44:8080 weight=7; 40 | server 172.24.83.45:8080 weight=1; 41 | } 42 | 43 | server { 44 | listen 443 ssl http2; 45 | server_name isucon9.catatsuy.org; 46 | 47 | ssl_certificate /etc/nginx/ssl/fullchain.pem; 48 | ssl_certificate_key /etc/nginx/ssl/privkey.pem; 49 | 50 | root /home/isucon/isucari/webapp/public; 51 | location /static/ { 52 | add_header Cache-Control "public max-age=86400"; 53 | } 54 | location /upload/ { 55 | add_header Cache-Control "public max-age=86400"; 56 | } 57 | 58 | location / { 59 | proxy_pass http://app; 60 | proxy_set_header Host $host; 61 | } 62 | location /login { 63 | proxy_pass http://login_app; 64 | proxy_set_header Host $host; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/error/InternalServerErrorPage/InternalServerErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MuiThemeProvider, Theme } from '@material-ui/core'; 3 | import { themeInstance } from '../../../theme'; 4 | import Container from '@material-ui/core/Container'; 5 | import makeStyles from '@material-ui/core/styles/makeStyles'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import { Link } from 'react-router-dom'; 8 | import { routes } from '../../../routes/Route'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | container: { 12 | paddingTop: theme.spacing(2), 13 | display: 'flex', 14 | flexDirection: 'column', 15 | alignItems: 'center', 16 | }, 17 | img: { 18 | width: '70%', 19 | }, 20 | message: { 21 | paddingTop: theme.spacing(1), 22 | }, 23 | link: { 24 | paddingTop: theme.spacing(2), 25 | }, 26 | })); 27 | 28 | export type Props = { 29 | message?: string; 30 | }; 31 | 32 | const InternalServerErrorPage: React.FC = ({ message }) => { 33 | const classes = useStyles(); 34 | 35 | return ( 36 | 37 | 38 | {'not 43 | Internal Server Error 44 | {message && ( 45 | 46 | {message} 47 | 48 | )} 49 | 50 | 51 | トップページへ 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export { InternalServerErrorPage }; 60 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemImage/ItemImage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from '@material-ui/core/Card'; 3 | import makeStyles from '@material-ui/core/styles/makeStyles'; 4 | import { Theme } from '@material-ui/core'; 5 | import Typography from '@material-ui/core/Typography'; 6 | 7 | const getUserStyles = (width: number) => 8 | makeStyles((theme: Theme) => ({ 9 | card: { 10 | width: `${width}px`, 11 | position: 'relative', 12 | }, 13 | itemImage: { 14 | width: `${width}px`, 15 | height: 'auto', 16 | }, 17 | soldOut: { 18 | position: 'absolute', 19 | top: 0, 20 | right: 0, 21 | zIndex: 1, 22 | width: 0, 23 | height: 0, 24 | borderStyle: 'solid', 25 | borderWidth: `0 140px 140px 0`, 26 | borderColor: 'transparent #ff0000 transparent transparent;', 27 | }, 28 | soldOutText: { 29 | position: 'absolute', 30 | top: '35px', 31 | right: '1px', 32 | fontWeight: theme.typography.fontWeightBold, 33 | zIndex: 2, 34 | transform: 'rotate(45deg);', 35 | color: theme.palette.primary.contrastText, 36 | }, 37 | })); 38 | 39 | interface Props { 40 | imageUrl: string; 41 | title: string; 42 | isSoldOut: boolean; 43 | width: number; 44 | } 45 | 46 | const ItemImage: React.FC = ({ imageUrl, title, isSoldOut, width }) => { 47 | const classes = getUserStyles(width)(); 48 | 49 | return ( 50 | 51 | {isSoldOut && ( 52 | 53 |
54 | 55 | SOLD OUT 56 | 57 | 58 | )} 59 | {title} 60 | 61 | ); 62 | }; 63 | 64 | export { ItemImage }; 65 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/errorReducer.ts: -------------------------------------------------------------------------------- 1 | import { INTERNAL_SERVER_ERROR, NOT_FOUND_ERROR } from '../actions/errorAction'; 2 | import { FETCH_ITEM_FAIL } from '../actions/fetchItemAction'; 3 | import { FETCH_SETTINGS_FAIL } from '../actions/settingsAction'; 4 | import { FETCH_TRANSACTIONS_FAIL } from '../actions/fetchTransactionsAction'; 5 | import { FETCH_USER_ITEMS_FAIL } from '../actions/fetchUserItemsAction'; 6 | import { FETCH_USER_PAGE_DATA_FAIL } from '../actions/fetchUserPageDataAction'; 7 | import { FETCH_TIMELINE_FAIL } from '../actions/fetchTimelineAction'; 8 | import { ActionTypes } from '../actions/actionTypes'; 9 | 10 | export const NoError = 'NO_ERROR'; 11 | export const NotFoundError = 'NOT_FOUND'; 12 | export const InternalServerError = 'INTERNAL_SERVER_ERROR'; 13 | export type ErrorType = 14 | | typeof NoError 15 | | typeof NotFoundError 16 | | typeof InternalServerError; 17 | 18 | export interface ErrorState { 19 | errorType: ErrorType; 20 | errorCode?: number; 21 | errorMessage?: string; 22 | } 23 | 24 | const initialState: ErrorState = { 25 | errorType: NoError, 26 | }; 27 | 28 | const error = ( 29 | state: ErrorState = initialState, 30 | action: ActionTypes, 31 | ): ErrorState => { 32 | switch (action.type) { 33 | case NOT_FOUND_ERROR: 34 | return { 35 | errorType: NotFoundError, 36 | errorCode: 404, 37 | errorMessage: action.message, 38 | }; 39 | case INTERNAL_SERVER_ERROR: 40 | case FETCH_ITEM_FAIL: 41 | case FETCH_TIMELINE_FAIL: 42 | case FETCH_TRANSACTIONS_FAIL: 43 | case FETCH_USER_ITEMS_FAIL: 44 | case FETCH_USER_PAGE_DATA_FAIL: 45 | case FETCH_SETTINGS_FAIL: 46 | return { 47 | errorType: InternalServerError, 48 | errorCode: 500, 49 | errorMessage: action.message, 50 | }; 51 | default: 52 | return { errorType: NoError }; 53 | } 54 | }; 55 | 56 | export default error; 57 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionList/TransactionList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TransactionItem } from '../../dataObjects/item'; 3 | import makeStyles from '@material-ui/core/styles/makeStyles'; 4 | import GridList from '@material-ui/core/GridList'; 5 | import GridListTile from '@material-ui/core/GridListTile'; 6 | import InfiniteScroll from 'react-infinite-scroller'; 7 | import TransactionContainer from '../../containers/TransactionContainer'; 8 | import { Theme } from '@material-ui/core'; 9 | import { TimelineLoading } from '../TimelineLoading'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | grid: { 13 | width: '900px', 14 | height: '300px', 15 | }, 16 | tile: { 17 | overflow: 'inherit', 18 | }, 19 | })); 20 | 21 | interface Props { 22 | items: TransactionItem[]; 23 | hasNext: boolean; 24 | loadMore: (createdAt: number, itemId: number, page: number) => void; 25 | } 26 | 27 | const TransactionList: React.FC = function({ 28 | items, 29 | hasNext, 30 | loadMore, 31 | }: Props) { 32 | const classes = useStyles(); 33 | 34 | const transactionsComponents = []; 35 | 36 | for (const item of items) { 37 | transactionsComponents.push( 38 | 43 | 44 | , 45 | ); 46 | } 47 | 48 | const lastItem = items[items.length - 1]; 49 | 50 | return ( 51 | } 56 | > 57 | 58 | {transactionsComponents} 59 | 60 | 61 | ); 62 | }; 63 | 64 | export { TransactionList }; 65 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/authStatusReducer.ts: -------------------------------------------------------------------------------- 1 | import { LOGIN_SUCCESS } from '../actions/authenticationActions'; 2 | import { REGISTER_SUCCESS } from '../actions/registerAction'; 3 | import { 4 | FETCH_SETTINGS_FAIL, 5 | FETCH_SETTINGS_SUCCESS, 6 | } from '../actions/settingsAction'; 7 | import { UserData } from '../dataObjects/user'; 8 | import { ActionTypes } from '../actions/actionTypes'; 9 | 10 | export interface AuthStatusState { 11 | userId?: number; 12 | accountName?: string; 13 | address?: string; 14 | numSellItems?: number; 15 | checked: boolean; // 初回のsettings取得が完了したかどうか 16 | } 17 | 18 | const initialState: AuthStatusState = { 19 | checked: false, 20 | }; 21 | 22 | const authStatus = ( 23 | state: AuthStatusState = initialState, 24 | action: ActionTypes, 25 | ): AuthStatusState => { 26 | switch (action.type) { 27 | case LOGIN_SUCCESS: 28 | case REGISTER_SUCCESS: { 29 | return { 30 | ...state, 31 | ...action.payload, 32 | }; 33 | } 34 | case FETCH_SETTINGS_SUCCESS: { 35 | const user: UserData | undefined = action.payload.settings.user; 36 | let userPayload: 37 | | { 38 | userId: number; 39 | accountName: string; 40 | address?: string; 41 | numSellItems: number; 42 | } 43 | | {} = {}; 44 | 45 | if (user) { 46 | userPayload = { 47 | userId: user.id, 48 | accountName: user.accountName, 49 | address: user.address || undefined, 50 | numSellItems: user.numSellItems, 51 | }; 52 | } 53 | 54 | return { 55 | ...state, 56 | ...userPayload, 57 | checked: true, 58 | }; 59 | } 60 | case FETCH_SETTINGS_FAIL: { 61 | return { 62 | ...state, 63 | checked: true, 64 | }; 65 | } 66 | default: 67 | return state; 68 | } 69 | }; 70 | 71 | export default authStatus; 72 | -------------------------------------------------------------------------------- /isucari/webapp/public/index.html: -------------------------------------------------------------------------------- 1 | ISUCARI
-------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/actionTypes.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | import { AuthActions } from './authenticationActions'; 3 | import { BuyActions } from './buyAction'; 4 | import { ErrorActions } from './errorAction'; 5 | import { FetchTimelineActions } from './fetchTimelineAction'; 6 | import { FetchTransactionActions } from './fetchTransactionsAction'; 7 | import { FetchUserItemsActions } from './fetchUserItemsAction'; 8 | import { LocationChangeActions } from './locationChangeAction'; 9 | import { PostBumpActions } from './postBumpAction'; 10 | import { FetchUserPageDataActions } from './fetchUserPageDataAction'; 11 | import { PostCompleteActions } from './postCompleteAction'; 12 | import { PostItemEditActions } from './postItemEditAction'; 13 | import { PostShippedActions } from './postShippedAction'; 14 | import { PostShippedDoneActions } from './postShippedDoneAction'; 15 | import { RegisterActions } from './registerAction'; 16 | import { SellingItemActions } from './sellingItemAction'; 17 | import { SettingsActions } from './settingsAction'; 18 | import { FetchItemActions } from './fetchItemAction'; 19 | import { RouterAction } from 'connected-react-router'; 20 | import { SnackBarActions } from './snackBarAction'; 21 | import { SnackBarVariant } from '../components/SnackBar'; 22 | 23 | type LibraryActions = RouterAction; 24 | 25 | export type ActionTypes = 26 | | LibraryActions 27 | | AuthActions 28 | | BuyActions 29 | | ErrorActions 30 | | FetchItemActions 31 | | FetchTimelineActions 32 | | FetchTransactionActions 33 | | FetchUserItemsActions 34 | | FetchUserPageDataActions 35 | | LocationChangeActions 36 | | PostBumpActions 37 | | PostCompleteActions 38 | | PostItemEditActions 39 | | PostShippedActions 40 | | PostShippedDoneActions 41 | | RegisterActions 42 | | SellingItemActions 43 | | SettingsActions 44 | | SnackBarActions; 45 | 46 | export interface SnackBarAction extends Action { 47 | snackBarMessage: string; 48 | variant: SnackBarVariant; 49 | } 50 | -------------------------------------------------------------------------------- /isucari/webapp/docs/APPLICATION_SPEC.md: -------------------------------------------------------------------------------- 1 | # ISUCARI アプリケーション仕様書 2 | 3 | ISUCARIロゴ 4 | 5 | ISUCARIは椅子を売りたい人/買いたい人をつなげるフリマアプリです。 6 | 従来のECサービスと比べて以下の特徴があります。 7 | 8 | * 安心安全の決済基盤 9 | * 匿名配送により住所を伝えなくても取引が可能に 10 | * 買いたい/売りたいと思った時にすぐに使えるシンプルさ 11 | 12 | ## ISUCARIの使い方 13 | 14 | ### 椅子を売ろう! 15 | 16 | #### まずは椅子を出品しよう! 17 | 18 | 1. 椅子の情報をいれよう! 19 | - タイムラインページの右下の出品ボタンを押すと出品画面にいくよ! 20 | - シンプルなフォームに情報を入力すれば即出品♪ 21 | - ![1-1](images/1-1.png) 22 | 1. 売れるのを待とう! 23 | - あなたの椅子が買われるのを楽しみに待とう♪ 24 | 1. 椅子を発送しよう! 25 | - 無事購入されたら椅子を発送しよう! 26 | - 商品ページかマイページから取引画面に行こう👀 27 | 28 | #### 売れた椅子を発送しよう! 29 | 30 | 1. 集荷予約をしよう! 31 | - 集荷予約をして椅子を送る準備をしよう😤 32 | - 集荷予約は取引画面からできるぞ! 33 | - ![2-1](images/2-1.png) 34 | 1. 配達員に椅子をわたそう! 35 | - 配達員が来たらQRコードを見せよう📱 36 | - 椅子を渡したら発送完了ボタンを押そう♪ 37 | - ![2-2](images/2-2.png) 38 | 1. 購入者の受け取りを待とう! 39 | - 椅子が届くのをまとう♪ 40 | - 届いたかどうかは取引画面で確認できるぞ! 41 | - 購入者が椅子を受け取ったら取引完了♪ 42 | 43 | ### 椅子を買おう! 44 | 45 | 1. ほしい椅子を探そう! 46 | - タイムライン、カテゴリタイムラインから好みの椅子を探そう👀 47 | - カテゴリタイムラインへはサイドバーからいけるよ! 48 | - ![3-1](images/3-1.png) 49 | 1. 椅子を買おう! 50 | - 運命の椅子を見つけたら購入しよう😎 51 | - カード番号を入力して簡単1ステップ購入! 52 | - ![3-2](images/3-2.png) 53 | 1. 椅子が届くのを待とう⏱ 54 | - 出品者が発送するのを待とう! 55 | - 発送されたかどうかは取引画面で確認できるぞ! 56 | 1. 取引を完了しよう! 57 | - 椅子が届いたら「取引完了」をしよう! 58 | - これで取引完了♪ 59 | 60 | ## キャンペーン機能について 61 | 62 | マニュアルを参照 63 | 64 | ## 外部サービスの仕様 65 | 66 | [外部サービス仕様書](EXTERNAL_SERVICE_SPEC.md) を参照 67 | 68 | ## ISUCARI ステータス遷移表 69 | 70 | | | WHO | items | transaction_evidences | shippings | 71 | |------------------------|:------:|:--------:|:---------------------:|:-------------------:| 72 | | /sell (出品) | 出品者 | on_sale | - | - | 73 | | /buy (購入) | 購入者 | trading | wait_shipping | initial | 74 | | /ship (集荷予約) | 出品者 | ↓ | ↓ | wait_pickup | 75 | | /ship_done (発送完了)| 出品者 | ↓ | wait_done | shipping or done | 76 | | /complete (取引完了) | 購入者 | sold_out | done | done | 77 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/reducers/pageReducuer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_ITEM_FAIL, 3 | FETCH_ITEM_START, 4 | FETCH_ITEM_SUCCESS, 5 | } from '../actions/fetchItemAction'; 6 | import { 7 | FETCH_SETTINGS_FAIL, 8 | FETCH_SETTINGS_START, 9 | FETCH_SETTINGS_SUCCESS, 10 | } from '../actions/settingsAction'; 11 | import { 12 | FETCH_TIMELINE_FAIL, 13 | FETCH_TIMELINE_SUCCESS, 14 | } from '../actions/fetchTimelineAction'; 15 | import { 16 | FETCH_USER_PAGE_DATA_FAIL, 17 | FETCH_USER_PAGE_DATA_START, 18 | FETCH_USER_PAGE_DATA_SUCCESS, 19 | } from '../actions/fetchUserPageDataAction'; 20 | import { PATH_NAME_CHANGE } from '../actions/locationChangeAction'; 21 | import { ActionTypes } from '../actions/actionTypes'; 22 | 23 | export interface PageState { 24 | isLoading: boolean; 25 | isItemLoading: boolean; 26 | isTimelineLoading: boolean; 27 | isUserPageLoading: boolean; 28 | } 29 | 30 | const initialState: PageState = { 31 | isLoading: true, 32 | isItemLoading: true, 33 | isTimelineLoading: true, 34 | isUserPageLoading: true, 35 | }; 36 | 37 | const pathChangeState: PageState = { 38 | isLoading: false, // Settings取得時しかtrueにならない 39 | isItemLoading: true, 40 | isTimelineLoading: true, 41 | isUserPageLoading: true, 42 | }; 43 | 44 | const page = ( 45 | state: PageState = initialState, 46 | action: ActionTypes, 47 | ): PageState => { 48 | switch (action.type) { 49 | // Item page 50 | case FETCH_ITEM_START: 51 | return { ...state, isItemLoading: true }; 52 | case FETCH_ITEM_SUCCESS: 53 | case FETCH_ITEM_FAIL: 54 | return { ...state, isItemLoading: false }; 55 | // Timeline 56 | case FETCH_TIMELINE_SUCCESS: 57 | case FETCH_TIMELINE_FAIL: 58 | return { ...state, isTimelineLoading: false }; 59 | // Settings 60 | case FETCH_SETTINGS_START: 61 | return { ...state, isLoading: true }; 62 | case FETCH_SETTINGS_SUCCESS: 63 | case FETCH_SETTINGS_FAIL: 64 | return { ...state, isLoading: false }; 65 | // User page 66 | case FETCH_USER_PAGE_DATA_START: 67 | return { ...state, isUserPageLoading: true }; 68 | case FETCH_USER_PAGE_DATA_SUCCESS: 69 | case FETCH_USER_PAGE_DATA_FAIL: 70 | return { ...state, isUserPageLoading: false }; 71 | // Location change 72 | case PATH_NAME_CHANGE: 73 | return pathChangeState; 74 | default: 75 | return { ...state }; 76 | } 77 | }; 78 | 79 | export default page; 80 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionComponent/TransactionComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from '@material-ui/core/Card'; 3 | import makeStyles from '@material-ui/core/styles/makeStyles'; 4 | import { TransactionItem } from '../../dataObjects/item'; 5 | import CardMedia from '@material-ui/core/CardMedia/CardMedia'; 6 | import CardContent from '@material-ui/core/CardContent/CardContent'; 7 | import Typography from '@material-ui/core/Typography/Typography'; 8 | import { TransactionLabel } from '../TransactionLabel'; 9 | import { Theme } from '@material-ui/core'; 10 | import CardActionArea from '@material-ui/core/CardActionArea'; 11 | 12 | const MAX_ITEM_NAME_LENGTH = 30; 13 | 14 | const useStyles = makeStyles((theme: Theme) => ({ 15 | card: { 16 | display: 'flex', 17 | justifyContent: 'flex-start', 18 | flexDirection: 'row', 19 | }, 20 | detail: { 21 | display: 'flex', 22 | flexDirection: 'column', 23 | }, 24 | itemTitle: { 25 | paddingLeft: theme.spacing(1), 26 | paddingRight: theme.spacing(3), 27 | paddingBottom: theme.spacing(2), 28 | }, 29 | img: { 30 | width: '130px', 31 | height: '130px', 32 | }, 33 | })); 34 | 35 | interface Props { 36 | item: TransactionItem; 37 | onClickCard: (item: TransactionItem) => void; 38 | } 39 | 40 | const TransactionComponent: React.FC = ({ item, onClickCard }) => { 41 | const classes = useStyles(); 42 | const onClick = (e: React.MouseEvent) => { 43 | e.preventDefault(); 44 | onClickCard(item); 45 | }; 46 | 47 | return ( 48 | 49 | 50 | 55 |
56 | 57 | 62 | {item.name.length > MAX_ITEM_NAME_LENGTH 63 | ? item.name.substr(0, MAX_ITEM_NAME_LENGTH) + '...' 64 | : item.name} 65 | 66 | 67 | 68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export { TransactionComponent }; 75 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/TransactionLabel/TransactionLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import makeStyles from '@material-ui/core/styles/makeStyles'; 3 | import { ItemStatus } from '../../dataObjects/item'; 4 | import { Theme } from '@material-ui/core'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import Card from '@material-ui/core/Card'; 7 | 8 | const baseWidth = '80px'; 9 | const baseHeight = '25px'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | container: { 13 | display: 'flex', 14 | flexDirection: 'column', 15 | }, 16 | normalLabel: { 17 | width: baseWidth, 18 | height: baseHeight, 19 | color: theme.palette.secondary.contrastText, 20 | backgroundColor: theme.palette.secondary.main, 21 | padding: theme.spacing(1), 22 | textAlign: 'center', 23 | }, 24 | soldOutLabel: { 25 | width: baseWidth, 26 | height: baseHeight, 27 | color: theme.palette.text.primary, 28 | backgroundColor: theme.palette.grey.A100, 29 | padding: theme.spacing(1), 30 | textAlign: 'center', 31 | }, 32 | tradingLabel: { 33 | width: baseWidth, 34 | height: baseHeight, 35 | color: theme.palette.primary.contrastText, 36 | backgroundColor: theme.palette.primary.main, 37 | padding: theme.spacing(1), 38 | textAlign: 'center', 39 | }, 40 | })); 41 | 42 | interface Props { 43 | itemStatus: ItemStatus; 44 | } 45 | 46 | const getLabelByStatus = ( 47 | status: ItemStatus, 48 | ): [string, 'normalLabel' | 'soldOutLabel' | 'tradingLabel'] => { 49 | switch (status) { 50 | case 'on_sale': 51 | return ['販売中', 'normalLabel']; 52 | case 'trading': 53 | return ['取引中', 'tradingLabel']; 54 | case 'sold_out': 55 | return ['売り切れ', 'soldOutLabel']; 56 | case 'stop': 57 | return ['出品停止中', 'normalLabel']; 58 | case 'cancel': 59 | return ['キャンセル', 'normalLabel']; 60 | } 61 | }; 62 | 63 | const TransactionLabel: React.FC = ({ itemStatus }) => { 64 | const classes = useStyles(); 65 | const [labelName, classKey] = getLabelByStatus(itemStatus); 66 | const className = classes[classKey]; 67 | 68 | return ( 69 |
70 | 71 | 72 | {labelName} 73 | 74 | 75 |
76 | ); 77 | }; 78 | 79 | export { TransactionLabel }; 80 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/postBumpAction.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { Action } from 'redux'; 4 | import { ErrorRes, BumpReq, BumpRes } from '../types/appApiTypes'; 5 | import { AppResponseError } from '../errors/AppResponseError'; 6 | import { AppState } from '../index'; 7 | import { SnackBarAction } from './actionTypes'; 8 | 9 | export const POST_BUMP_START = 'POST_BUMP_START'; 10 | export const POST_BUMP_SUCCESS = 'POST_BUMP_SUCCESS'; 11 | export const POST_BUMP_FAIL = 'POST_BUMP_FAIL'; 12 | 13 | export type PostBumpActions = 14 | | PostBumpStartAction 15 | | PostBumpSuccessAction 16 | | PostBumpFailAction; 17 | 18 | type ThunkResult = ThunkAction; 19 | 20 | export function postBumpAction(itemId: number): ThunkResult { 21 | return (dispatch: ThunkDispatch) => { 22 | Promise.resolve() 23 | .then(() => { 24 | dispatch(postBumpStartAction()); 25 | }) 26 | .then(() => { 27 | return AppClient.post('/bump', { item_id: itemId } as BumpReq); 28 | }) 29 | .then(async (response: Response) => { 30 | if (response.status !== 200) { 31 | const errRes: ErrorRes = await response.json(); 32 | throw new AppResponseError(errRes.error, response); 33 | } 34 | 35 | return await response.json(); 36 | }) 37 | .then((body: BumpRes) => { 38 | dispatch(postBumpSuccessAction()); 39 | }) 40 | .catch((err: Error) => { 41 | dispatch(postBumpFailAction(err.message)); 42 | }); 43 | }; 44 | } 45 | 46 | export interface PostBumpStartAction extends Action {} 47 | 48 | export function postBumpStartAction(): PostBumpStartAction { 49 | return { 50 | type: POST_BUMP_START, 51 | }; 52 | } 53 | 54 | export interface PostBumpSuccessAction 55 | extends SnackBarAction {} 56 | 57 | export function postBumpSuccessAction(): PostBumpSuccessAction { 58 | return { 59 | type: POST_BUMP_SUCCESS, 60 | snackBarMessage: 'BUMPに成功しました', 61 | variant: 'success', 62 | }; 63 | } 64 | 65 | export interface PostBumpFailAction 66 | extends SnackBarAction {} 67 | 68 | export function postBumpFailAction(error: string): PostBumpFailAction { 69 | return { 70 | type: POST_BUMP_FAIL, 71 | snackBarMessage: error, 72 | variant: 'error', 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/ItemFooter/ItemFooter.tsx: -------------------------------------------------------------------------------- 1 | import { AppBar, Theme } from '@material-ui/core'; 2 | import makeStyles from '@material-ui/core/styles/makeStyles'; 3 | import Grid from '@material-ui/core/Grid'; 4 | import * as React from 'react'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import Button from '@material-ui/core/Button'; 7 | import Tooltip from '@material-ui/core/Tooltip'; 8 | import { ReactElement } from 'react'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | appBar: { 12 | top: 'auto', 13 | bottom: 0, 14 | padding: theme.spacing(0, 2), 15 | }, 16 | buyButton: { 17 | margin: theme.spacing(1), 18 | }, 19 | })); 20 | 21 | type Props = { 22 | price: number; 23 | buttons: { 24 | onClick: (e: React.MouseEvent) => void; 25 | buttonText: string; 26 | disabled: boolean; 27 | tooltip?: ReactElement; 28 | }[]; 29 | }; 30 | 31 | const ItemFooter: React.FC = ({ price, buttons }) => { 32 | const classes = useStyles(); 33 | 34 | return ( 35 | 36 | 43 | 44 | {price}イスコイン 45 | 46 | 47 | 48 | {buttons.map(button => { 49 | const ButtonComponent = ( 50 | 59 | ); 60 | 61 | // Hack: いいコードじゃないけど時間ないので許して 62 | if (button.tooltip) { 63 | return ( 64 | 65 | 66 | {ButtonComponent} 67 | 68 | 69 | ); 70 | } 71 | 72 | return {ButtonComponent}; 73 | })} 74 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export { ItemFooter }; 82 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/ItemListPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TimelineItem } from '../dataObjects/item'; 3 | import { ItemList } from '../components/ItemList'; 4 | import SellingButtonContainer from '../containers/SellingButtonContainer'; 5 | import { ErrorProps, PageComponentWithError } from '../hoc/withBaseComponent'; 6 | import BasePageContainer from '../containers/BasePageContainer'; 7 | import { createStyles, Theme, WithStyles } from '@material-ui/core'; 8 | import { StyleRules } from '@material-ui/core/styles'; 9 | import withStyles from '@material-ui/core/styles/withStyles'; 10 | import LoadingComponent from '../components/LoadingComponent'; 11 | import Typography from '@material-ui/core/Typography/Typography'; 12 | 13 | const styles = (theme: Theme): StyleRules => 14 | createStyles({ 15 | root: { 16 | display: 'flex', 17 | flexWrap: 'wrap', 18 | marginTop: theme.spacing(1), 19 | justifyContent: 'space-around', 20 | overflow: 'hidden', 21 | }, 22 | }); 23 | 24 | interface ItemListPageProps extends WithStyles { 25 | loading: boolean; 26 | load: () => void; 27 | items: TimelineItem[]; 28 | hasNext: boolean; 29 | loadMore: (createdAt: number, itemId: number, page: number) => void; 30 | } 31 | 32 | type Props = ItemListPageProps & ErrorProps; 33 | 34 | class ItemListPage extends React.Component { 35 | constructor(props: Props) { 36 | super(props); 37 | 38 | this.props.load(); 39 | } 40 | 41 | render() { 42 | const { classes, loading, items, loadMore, hasNext } = this.props; 43 | 44 | const Content: React.FC<{}> = () => { 45 | if (items.length === 0) { 46 | return ( 47 |
48 | 出品されている商品はありません 49 | 50 |
51 | ); 52 | } 53 | 54 | const lastItem = items[items.length - 1]; 55 | const loadMoreItems = loadMore.bind( 56 | null, 57 | lastItem.createdAt, 58 | lastItem.id, 59 | ); 60 | return ( 61 |
62 | 63 | 64 |
65 | ); 66 | }; 67 | 68 | return ( 69 | 70 | {loading ? : } 71 | 72 | ); 73 | } 74 | } 75 | 76 | export default PageComponentWithError()(withStyles(styles)(ItemListPage)); 77 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/httpClients/appClient.ts: -------------------------------------------------------------------------------- 1 | import { SettingsRes } from '../types/appApiTypes'; 2 | import { AppResponseError } from '../errors/AppResponseError'; 3 | 4 | /** 5 | * HTTP client for main app 6 | */ 7 | class AppClient { 8 | private defaultHeaders: HeadersInit = {}; 9 | 10 | async get(path: string, params: Record = {}): Promise { 11 | let getParams = new URLSearchParams(); 12 | for (const key in params) { 13 | const value = params[key]; 14 | if (value !== undefined) { 15 | getParams.set(key, params[key]); 16 | } 17 | } 18 | 19 | let url = `${path}`; 20 | if (getParams.toString() !== '') { 21 | url = `${url}?${getParams.toString()}`; 22 | } 23 | 24 | return await fetch(url, { 25 | method: 'GET', 26 | headers: this.defaultHeaders, 27 | }); 28 | } 29 | 30 | async post( 31 | path: string, 32 | params: any = {}, 33 | csrfCheckRequired: boolean = true, 34 | ): Promise { 35 | let requestOption: RequestInit = { 36 | method: 'POST', 37 | mode: 'same-origin', 38 | headers: Object.assign({}, this.defaultHeaders, { 39 | 'Content-Type': 'application/json', 40 | }), 41 | credentials: 'same-origin', 42 | }; 43 | 44 | if (csrfCheckRequired) { 45 | params.csrf_token = await this.getCsrfToken(); 46 | } 47 | 48 | requestOption.body = JSON.stringify(params); 49 | 50 | return await fetch(path, requestOption); 51 | } 52 | 53 | async postFormData(path: string, body: FormData): Promise { 54 | let requestOption: RequestInit = { 55 | method: 'POST', 56 | mode: 'same-origin', 57 | // MEMO: The reason why we should not set Content-Type header by ourselves 58 | // https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post 59 | headers: this.defaultHeaders, 60 | credentials: 'same-origin', 61 | }; 62 | 63 | body.append('csrf_token', await this.getCsrfToken()); 64 | requestOption.body = body; 65 | 66 | return await fetch(path, requestOption); 67 | } 68 | 69 | private async getCsrfToken(): Promise { 70 | const res: Response = await fetch('/settings', { 71 | method: 'GET', 72 | headers: this.defaultHeaders, 73 | }); 74 | 75 | if (!res.ok) { 76 | throw new AppResponseError('CSRF tokenの取得に失敗しました', res); 77 | } 78 | 79 | const body: SettingsRes = await res.json(); 80 | 81 | return body.csrf_token; 82 | } 83 | } 84 | 85 | export default new AppClient(); 86 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/authenticationActions.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { CallHistoryMethodAction, push } from 'connected-react-router'; 4 | import { routes } from '../routes/Route'; 5 | import { AppState } from '../index'; 6 | import { ErrorRes, LoginRes } from '../types/appApiTypes'; 7 | import { AppResponseError } from '../errors/AppResponseError'; 8 | import { SnackBarAction } from './actionTypes'; 9 | 10 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 11 | export const LOGIN_FAIL = 'LOGIN_FAIL'; 12 | 13 | export type AuthActions = 14 | | LoginSuccessAction 15 | | LoginFailAction 16 | | CallHistoryMethodAction; 17 | 18 | type ThunkResult = ThunkAction; 19 | 20 | export function postLoginAction( 21 | accountName: string, 22 | password: string, 23 | ): ThunkResult { 24 | return (dispatch: ThunkDispatch) => { 25 | AppClient.post( 26 | '/login', 27 | { 28 | account_name: accountName, 29 | password: password, 30 | }, 31 | false, 32 | ) 33 | .then(async (response: Response) => { 34 | if (response.status !== 200) { 35 | const errRes: ErrorRes = await response.json(); 36 | throw new AppResponseError(errRes.error, response); 37 | } 38 | 39 | return await response.json(); 40 | }) 41 | .then((body: LoginRes) => { 42 | dispatch( 43 | loginSuccessAction({ 44 | userId: body.id, 45 | accountName: body.account_name, 46 | address: body.address, 47 | }), 48 | ); 49 | dispatch(push(routes.top.path)); 50 | }) 51 | .catch((err: Error) => { 52 | dispatch(loginFailAction(err.message)); 53 | }); 54 | }; 55 | } 56 | 57 | export interface LoginSuccessAction { 58 | type: typeof LOGIN_SUCCESS; 59 | payload: { 60 | userId: number; 61 | accountName: string; 62 | address?: string; 63 | }; 64 | } 65 | 66 | export function loginSuccessAction(newAuthState: { 67 | userId: number; 68 | accountName: string; 69 | address?: string; 70 | }): LoginSuccessAction { 71 | return { 72 | type: LOGIN_SUCCESS, 73 | payload: newAuthState, 74 | }; 75 | } 76 | 77 | export interface LoginFailAction extends SnackBarAction {} 78 | 79 | export function loginFailAction(error: string): LoginFailAction { 80 | return { 81 | type: LOGIN_FAIL, 82 | snackBarMessage: error, 83 | variant: 'error', 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/registerAction.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { Action } from 'redux'; 4 | import { CallHistoryMethodAction, push } from 'connected-react-router'; 5 | import { ErrorRes, RegisterReq, RegisterRes } from '../types/appApiTypes'; 6 | import { routes } from '../routes/Route'; 7 | import { AppResponseError } from '../errors/AppResponseError'; 8 | import { AppState } from '../index'; 9 | import { SnackBarAction } from './actionTypes'; 10 | 11 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS'; 12 | export const REGISTER_FAIL = 'REGISTER_FAIL'; 13 | 14 | export type RegisterActions = 15 | | RegisterSuccessAction 16 | | RegisterFailAction 17 | | CallHistoryMethodAction; 18 | type ThunkResult = ThunkAction; 19 | 20 | export function postRegisterAction(payload: RegisterReq): ThunkResult { 21 | return (dispatch: ThunkDispatch) => { 22 | AppClient.post('/register', payload, false) 23 | .then(async (response: Response) => { 24 | if (response.status !== 200) { 25 | const errRes: ErrorRes = await response.json(); 26 | throw new AppResponseError(errRes.error, response); 27 | } 28 | 29 | return await response.json(); 30 | }) 31 | .then((body: RegisterRes) => { 32 | dispatch( 33 | registerSuccessAction({ 34 | userId: body.id, 35 | accountName: body.account_name, 36 | address: body.address, 37 | numSellItems: body.num_sell_items, 38 | }), 39 | ); 40 | dispatch(push(routes.top.path)); 41 | }) 42 | .catch((err: Error) => { 43 | dispatch(registerFailAction(err.message)); 44 | }); 45 | }; 46 | } 47 | 48 | export interface RegisterSuccessAction extends Action { 49 | payload: { 50 | userId: number; 51 | accountName: string; 52 | address: string; 53 | }; 54 | } 55 | 56 | export function registerSuccessAction(newAuthState: { 57 | userId: number; 58 | accountName: string; 59 | address: string; 60 | numSellItems: number; 61 | }): RegisterSuccessAction { 62 | return { 63 | type: REGISTER_SUCCESS, 64 | payload: newAuthState, 65 | }; 66 | } 67 | 68 | export interface RegisterFailAction 69 | extends SnackBarAction {} 70 | 71 | export function registerFailAction(error: string): RegisterFailAction { 72 | return { 73 | type: REGISTER_FAIL, 74 | snackBarMessage: error, 75 | variant: 'error', 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/components/SnackBar/SnackBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Snackbar, Theme } from '@material-ui/core'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import CloseIcon from '@material-ui/icons/Close'; 5 | import CheckCircleIcon from '@material-ui/icons/CheckCircle'; 6 | import ErrorIcon from '@material-ui/icons/Error'; 7 | import makeStyles from '@material-ui/core/styles/makeStyles'; 8 | import SnackbarContent from '@material-ui/core/SnackbarContent'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | text: { 12 | display: 'flex', 13 | alignItems: 'center', 14 | }, 15 | close: { 16 | padding: theme.spacing(0.5), 17 | }, 18 | icon: { 19 | fontSize: 20, 20 | marginRight: theme.spacing(1), 21 | }, 22 | success: { 23 | backgroundColor: theme.palette.secondary.main, 24 | }, 25 | error: { 26 | backgroundColor: theme.palette.primary.main, 27 | }, 28 | })); 29 | 30 | export type SnackBarVariant = 'success' | 'error'; 31 | 32 | type Props = { 33 | open: boolean; 34 | variant: SnackBarVariant; 35 | message?: string; 36 | handleClose: (event: React.MouseEvent) => void; 37 | }; 38 | 39 | const getIcon = ( 40 | variant: SnackBarVariant, 41 | ): typeof CheckCircleIcon | typeof ErrorIcon => { 42 | switch (variant) { 43 | case 'success': 44 | return CheckCircleIcon; 45 | case 'error': 46 | return ErrorIcon; 47 | default: 48 | return CheckCircleIcon; 49 | } 50 | }; 51 | 52 | const SnackBar: React.FC = ({ open, variant, message, handleClose }) => { 53 | const classes = useStyles(); 54 | 55 | const handleOnClose = (event: React.SyntheticEvent, reason: string) => { 56 | return handleClose(event as React.MouseEvent); 57 | }; 58 | const Icon = getIcon(variant); 59 | 60 | return ( 61 | 70 | 74 | 75 | {message} 76 | 77 | } 78 | action={[ 79 | 86 | 87 | , 88 | ]} 89 | /> 90 | 91 | ); 92 | }; 93 | 94 | export { SnackBar }; 95 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/TopPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BasePageContainer from '../containers/BasePageContainer'; 3 | import { routes } from '../routes/Route'; 4 | import { Button, Theme } from '@material-ui/core'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import makeStyles from '@material-ui/core/styles/makeStyles'; 7 | import { Link, LinkProps } from 'react-router-dom'; 8 | 9 | const useStyles = makeStyles((theme: Theme) => ({ 10 | paper: { 11 | marginTop: theme.spacing(2), 12 | display: 'flex', 13 | flexDirection: 'column', 14 | alignItems: 'center', 15 | }, 16 | textarea: { 17 | marginTop: theme.spacing(1), 18 | marginBottom: theme.spacing(2), 19 | }, 20 | checklist: { 21 | marginTop: theme.spacing(2), 22 | marginBottom: theme.spacing(2), 23 | }, 24 | img: { 25 | width: '70%', 26 | }, 27 | button: { 28 | margin: theme.spacing(1), 29 | }, 30 | })); 31 | 32 | const TopPage: React.FC = () => { 33 | const classes = useStyles(); 34 | const LoginButtonLink = React.forwardRef( 35 | (props: LinkProps, ref: React.Ref) => ( 36 | 37 | ログイン 38 | 39 | ), 40 | ); 41 | const RegisterButtonLink = React.forwardRef( 42 | (props: LinkProps, ref: React.Ref) => ( 43 | 44 | 新規会員登録 45 | 46 | ), 47 | ); 48 | 49 | return ( 50 | 51 |
52 | {'ISUCARI'} 53 |
54 | 55 | 椅子限定のフリマサイト ついにリリース! 56 | 57 |
58 | ✔ 安全なカード決済 59 | ✔ お互い匿名で安心配送 60 |
61 | 62 | 安心安全にあなただけの椅子を手に入れよう! 63 | 64 |
65 |
84 |
85 | ); 86 | }; 87 | 88 | export default TopPage; 89 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/postShippedAction.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { Action } from 'redux'; 4 | import { ErrorRes, ShipReq, ShipRes } from '../types/appApiTypes'; 5 | import { fetchItemAction } from './fetchItemAction'; 6 | import { AppResponseError } from '../errors/AppResponseError'; 7 | import { AppState } from '../index'; 8 | import { SnackBarAction } from './actionTypes'; 9 | 10 | export const POST_SHIPPED_START = 'POST_SHIPPED_START'; 11 | export const POST_SHIPPED_SUCCESS = 'POST_SHIPPED_SUCCESS'; 12 | export const POST_SHIPPED_FAIL = 'POST_SHIPPED_FAIL'; 13 | 14 | export type PostShippedActions = 15 | | PostShippedStartAction 16 | | PostShippedSuccessAction 17 | | PostShippedFailAction; 18 | type ThunkResult = ThunkAction; 19 | 20 | export function postShippedAction(itemId: number): ThunkResult { 21 | return (dispatch: ThunkDispatch) => { 22 | Promise.resolve() 23 | .then(() => { 24 | dispatch(postShippedStartAction()); 25 | }) 26 | .then(() => { 27 | return AppClient.post('/ship', { 28 | item_id: itemId, 29 | } as ShipReq); 30 | }) 31 | .then(async (response: Response) => { 32 | if (response.status !== 200) { 33 | const errRes: ErrorRes = await response.json(); 34 | throw new AppResponseError(errRes.error, response); 35 | } 36 | 37 | return await response.json(); 38 | }) 39 | .then((body: ShipRes) => { 40 | dispatch(postShippedSuccessAction()); 41 | }) 42 | .then(() => { 43 | dispatch(fetchItemAction(itemId.toString())); // FIXME: 異常系のハンドリングが取引ページ向けでない 44 | }) 45 | .catch((err: Error) => { 46 | dispatch(postShippedFailAction(err.message)); 47 | }); 48 | }; 49 | } 50 | 51 | export interface PostShippedStartAction 52 | extends Action {} 53 | 54 | export function postShippedStartAction(): PostShippedStartAction { 55 | return { 56 | type: POST_SHIPPED_START, 57 | }; 58 | } 59 | 60 | export interface PostShippedSuccessAction 61 | extends Action {} 62 | 63 | export function postShippedSuccessAction(): PostShippedSuccessAction { 64 | return { 65 | type: POST_SHIPPED_SUCCESS, 66 | }; 67 | } 68 | 69 | export interface PostShippedFailAction 70 | extends SnackBarAction {} 71 | 72 | export function postShippedFailAction(error: string): PostShippedFailAction { 73 | return { 74 | type: POST_SHIPPED_FAIL, 75 | snackBarMessage: error, 76 | variant: 'error', 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/sellingItemAction.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { FormErrorState } from '../reducers/formErrorReducer'; 4 | import { CallHistoryMethodAction, push } from 'connected-react-router'; 5 | import { Action } from 'redux'; 6 | import { ErrorRes, SellRes } from '../types/appApiTypes'; 7 | import { routes } from '../routes/Route'; 8 | import { AppResponseError } from '../errors/AppResponseError'; 9 | import { AppState } from '../index'; 10 | 11 | export const SELLING_ITEM_SUCCESS = 'SELLING_ITEM_SUCCESS'; 12 | export const SELLING_ITEM_FAIL = 'SELLING_ITEM_FAIL'; 13 | 14 | export type SellingItemActions = 15 | | SellingSuccessAction 16 | | SellingFailAction 17 | | CallHistoryMethodAction; 18 | type ThunkResult = ThunkAction; 19 | 20 | export function listItemAction( 21 | name: string, 22 | description: string, 23 | price: number, 24 | categoryId: number, 25 | image: Blob, 26 | ): ThunkResult { 27 | return (dispatch: ThunkDispatch) => { 28 | const body = new FormData(); 29 | body.append('name', name); 30 | body.append('description', description); 31 | body.append('price', price.toString()); 32 | body.append('category_id', categoryId.toString()); 33 | body.append('image', image); 34 | AppClient.postFormData('/sell', body) 35 | .then(async (response: Response) => { 36 | if (!response.ok) { 37 | const errRes: ErrorRes = await response.json(); 38 | throw new AppResponseError(errRes.error, response); 39 | } 40 | return await response.json(); 41 | }) 42 | .then((body: SellRes) => { 43 | dispatch(sellingSuccessAction(body.id)); 44 | dispatch(push(routes.timeline.path)); 45 | }) 46 | .catch((err: Error) => { 47 | dispatch( 48 | sellingFailAction({ 49 | error: err.message, 50 | }), 51 | ); 52 | }); 53 | }; 54 | } 55 | 56 | export interface SellingSuccessAction 57 | extends Action { 58 | payload: { 59 | itemId: number; 60 | }; 61 | } 62 | 63 | export function sellingSuccessAction(itemId: number): SellingSuccessAction { 64 | return { 65 | type: SELLING_ITEM_SUCCESS, 66 | payload: { itemId }, 67 | }; 68 | } 69 | 70 | export interface SellingFailAction extends Action { 71 | payload: FormErrorState; 72 | } 73 | 74 | export function sellingFailAction( 75 | newErrors: FormErrorState, 76 | ): SellingFailAction { 77 | return { 78 | type: SELLING_ITEM_FAIL, 79 | payload: newErrors, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/pages/UserSettingPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BasePageContainer from '../containers/BasePageContainer'; 3 | import { Grid, Theme } from '@material-ui/core'; 4 | import Avatar from '@material-ui/core/Avatar'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import makeStyles from '@material-ui/core/styles/makeStyles'; 7 | import Divider from '@material-ui/core/Divider'; 8 | import { InternalServerErrorPage } from './error/InternalServerErrorPage'; 9 | import SellingButtonComponent from '../containers/SellingButtonContainer'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | avatar: { 13 | margin: theme.spacing(3), 14 | width: '100px', 15 | height: '100px', 16 | }, 17 | divider: { 18 | margin: theme.spacing(1), 19 | }, 20 | descSection: { 21 | marginTop: theme.spacing(3), 22 | marginBottom: theme.spacing(3), 23 | }, 24 | })); 25 | 26 | type Props = { 27 | id?: number; 28 | accountName?: string; 29 | address?: string; 30 | numSellItems?: number; 31 | }; 32 | 33 | const UserSettingPage: React.FC = ({ 34 | id, 35 | accountName, 36 | address, 37 | numSellItems, 38 | }) => { 39 | const classes = useStyles(); 40 | 41 | if ( 42 | id === undefined || 43 | accountName === undefined || 44 | address === undefined || 45 | numSellItems === undefined 46 | ) { 47 | return ( 48 | 49 | ); 50 | } 51 | 52 | return ( 53 | 54 | 62 | 63 | {accountName.charAt(0)} 64 | 65 | 66 | {accountName} 67 | 68 | 69 | 70 | 71 |
72 | 住所 73 | 74 | {address} 75 |
76 |
77 | 出品数 78 | 79 | {numSellItems} 80 |
81 |
82 |
83 | 84 |
85 | ); 86 | }; 87 | 88 | export default UserSettingPage; 89 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isucari-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.3.0", 7 | "@material-ui/icons": "^4.2.1", 8 | "@types/jest": "24.0.15", 9 | "@types/node": "12.6.8", 10 | "@types/react": "16.8.23", 11 | "@types/react-dom": "16.8.4", 12 | "@types/validator": "^10.11.2", 13 | "connected-react-router": "^6.5.2", 14 | "react": "^16.8.6", 15 | "react-dom": "^16.8.6", 16 | "react-infinite-scroller": "^1.2.4", 17 | "react-redux": "^7.1.0", 18 | "react-router-dom": "^5.0.1", 19 | "react-scripts": "3.0.1", 20 | "recompose": "^0.30.0", 21 | "redux": "^4.0.4", 22 | "redux-thunk": "^2.3.0", 23 | "typescript": "3.5.3", 24 | "validator": "^11.1.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "deploy:clean": "node ./tool/clean.js", 30 | "deploy:copy": "cp -r build/* ../public/", 31 | "deploy": "npm run build && npm run deploy:clean && npm run deploy:copy", 32 | "fix": "prettier --write './src/**/*.{ts,tsx}'", 33 | "lint": "npm run fix && eslint", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject", 36 | "watch": "npm-watch", 37 | "storybook": "start-storybook -s ./public -p 6006", 38 | "build-storybook": "build-storybook" 39 | }, 40 | "watch": { 41 | "build": "src/**/*" 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@babel/core": "^7.5.5", 60 | "@storybook/addon-actions": "^5.1.11", 61 | "@storybook/addon-links": "^5.1.11", 62 | "@storybook/addons": "^5.1.11", 63 | "@storybook/react": "^5.1.11", 64 | "@types/material-ui": "^0.21.6", 65 | "@types/react-infinite-scroller": "^1.2.1", 66 | "@types/react-redux": "^7.1.1", 67 | "@types/react-router-dom": "^4.3.4", 68 | "@types/recompose": "^0.30.6", 69 | "@types/redux": "^3.6.0", 70 | "@types/redux-thunk": "^2.1.0", 71 | "@types/storybook__react": "^4.0.2", 72 | "husky": "^3.0.3", 73 | "lint-staged": "^9.2.1", 74 | "npm-watch": "^0.6.0", 75 | "prettier": "^1.18.2", 76 | "redux-devtools-extension": "^2.13.8", 77 | "rimraf": "^2.6.3" 78 | }, 79 | "husky": { 80 | "hooks": { 81 | "pre-commit": "lint-staged" 82 | } 83 | }, 84 | "lint-staged": { 85 | "*.{ts,tsx}": [ 86 | "npm run lint", 87 | "git add" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/postCompleteAction.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { Action } from 'redux'; 4 | import { CompleteReq, CompleteRes, ErrorRes } from '../types/appApiTypes'; 5 | import { fetchItemAction } from './fetchItemAction'; 6 | import { AppResponseError } from '../errors/AppResponseError'; 7 | import { AppState } from '../index'; 8 | import { SnackBarAction } from './actionTypes'; 9 | 10 | export const POST_COMPLETE_START = 'POST_COMPLETE_START'; 11 | export const POST_COMPLETE_SUCCESS = 'POST_COMPLETE_SUCCESS'; 12 | export const POST_COMPLETE_FAIL = 'POST_COMPLETE_FAIL'; 13 | 14 | export type PostCompleteActions = 15 | | PostCompleteStartAction 16 | | PostCompleteSuccessAction 17 | | PostCompleteFailAction; 18 | type ThunkResult = ThunkAction; 19 | 20 | export function postCompleteAction(itemId: number): ThunkResult { 21 | return (dispatch: ThunkDispatch) => { 22 | Promise.resolve() 23 | .then(() => { 24 | dispatch(postCompleteStartAction()); 25 | }) 26 | .then(() => { 27 | return AppClient.post('/complete', { 28 | item_id: itemId, 29 | } as CompleteReq); 30 | }) 31 | .then(async (response: Response) => { 32 | if (response.status !== 200) { 33 | const errRes: ErrorRes = await response.json(); 34 | throw new AppResponseError(errRes.error, response); 35 | } 36 | 37 | return await response.json(); 38 | }) 39 | .then((body: CompleteRes) => { 40 | dispatch(postCompleteSuccessAction()); 41 | }) 42 | .then(() => { 43 | dispatch(fetchItemAction(itemId.toString())); // FIXME: 異常系のハンドリングが取引ページ向けでない 44 | }) 45 | .catch((err: Error) => { 46 | dispatch(postCompleteFailAction(err.message)); 47 | }); 48 | }; 49 | } 50 | 51 | export interface PostCompleteStartAction 52 | extends Action {} 53 | 54 | export function postCompleteStartAction(): PostCompleteStartAction { 55 | return { 56 | type: POST_COMPLETE_START, 57 | }; 58 | } 59 | 60 | export interface PostCompleteSuccessAction 61 | extends Action {} 62 | 63 | export function postCompleteSuccessAction(): PostCompleteSuccessAction { 64 | return { 65 | type: POST_COMPLETE_SUCCESS, 66 | }; 67 | } 68 | 69 | export interface PostCompleteFailAction 70 | extends SnackBarAction {} 71 | 72 | export function postCompleteFailAction(error: string): PostCompleteFailAction { 73 | return { 74 | type: POST_COMPLETE_FAIL, 75 | snackBarMessage: error, 76 | variant: 'error', 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /isucari/webapp/nodejs/api.ts: -------------------------------------------------------------------------------- 1 | 2 | import axios, {AxiosResponse} from "axios"; 3 | 4 | const client = axios.create({ responseType: 'json'}); 5 | 6 | const UserAgent = "isucon9-qualify-webapp"; 7 | const IsucariAPIToken = "Bearer 75ugk2m37a750fwir5xr-22l6h4wmue1bwrubzwd0"; 8 | 9 | type ShipmentCreateRequest = { 10 | to_address: string, 11 | to_name: string, 12 | from_address: string, 13 | from_name: string, 14 | } 15 | 16 | type ShipmentCreateResponse = { 17 | reserve_id: string, 18 | reserve_time: number, 19 | } 20 | 21 | type ShipmentRequestRequest = { 22 | reserve_id: string, 23 | } 24 | 25 | type ShipmentStatusRequest = { 26 | reserve_id: string, 27 | } 28 | 29 | type ShipmentStatusResponse = { 30 | status: string, 31 | reserve_time: number, 32 | } 33 | 34 | type PaymentTokenRequest = { 35 | shop_id: string, 36 | token: string, 37 | api_key: string, 38 | price: number, 39 | } 40 | 41 | type PaymentTokenResponse = { 42 | status: string, 43 | } 44 | 45 | export async function shipmentCreate(url: string, params: ShipmentCreateRequest): Promise { 46 | const res = await client.post(url + "/create", params, { 47 | headers: { 48 | 'User-Agent': UserAgent, 49 | 'Authorization': IsucariAPIToken, 50 | }, 51 | }); 52 | if (res.status !== 200) { 53 | throw res; 54 | } 55 | 56 | return res.data as ShipmentCreateResponse; 57 | } 58 | 59 | export async function shipmentRequest(url: string, params: ShipmentRequestRequest): Promise { 60 | const res = await client.post(url + "/request", params, { 61 | responseType: 'arraybuffer', 62 | headers: { 63 | 'User-Agent': UserAgent, 64 | 'Authorization': IsucariAPIToken, 65 | }, 66 | }); 67 | if (res.status !== 200) { 68 | throw res; 69 | } 70 | 71 | return res.data; 72 | } 73 | 74 | export async function shipmentStatus(url: string, params: ShipmentStatusRequest): Promise { 75 | const res = await client.post(url + "/status", params, { 76 | headers: { 77 | 'User-Agent': UserAgent, 78 | 'Authorization': IsucariAPIToken, 79 | }, 80 | }); 81 | if (res.status !== 200) { 82 | throw res; 83 | } 84 | 85 | return res.data as ShipmentStatusResponse; 86 | } 87 | 88 | export async function paymentToken(url: string, params: PaymentTokenRequest): Promise { 89 | const res = await client.post(url + "/token", params, { 90 | headers: { 91 | 'User-Agent': UserAgent, 92 | }, 93 | }); 94 | if (res.status !== 200) { 95 | throw res; 96 | } 97 | 98 | return res.data as PaymentTokenResponse; 99 | } 100 | 101 | 102 | -------------------------------------------------------------------------------- /isucari/webapp/ruby/lib/isucari/api.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'uri' 3 | require 'net/http' 4 | 5 | module Isucari 6 | class API 7 | class Error < StandardError; end 8 | 9 | ISUCARI_API_TOKEN = 'Bearer 75ugk2m37a750fwir5xr-22l6h4wmue1bwrubzwd0' 10 | 11 | def initialize 12 | @user_agent = 'isucon9-qualify-webapp' 13 | end 14 | 15 | def payment_token(payment_url, param) 16 | uri = URI.parse("#{payment_url}/token") 17 | 18 | req = Net::HTTP::Post.new(uri.path) 19 | req.body = param.to_json 20 | req['Content-Type'] = 'application/json' 21 | req['User-Agent'] = @user_agent 22 | 23 | http = Net::HTTP.new(uri.host, uri.port) 24 | http.use_ssl = uri.scheme == 'https' 25 | res = http.start { http.request(req) } 26 | 27 | if res.code != '200' 28 | raise Error, "status code #{res.code}; body #{res.body}" 29 | end 30 | 31 | JSON.parse(res.body) 32 | end 33 | 34 | def shipment_create(shipment_url, param) 35 | uri = URI.parse("#{shipment_url}/create") 36 | 37 | req = Net::HTTP::Post.new(uri.path) 38 | req.body = param.to_json 39 | req['Content-Type'] = 'application/json' 40 | req['User-Agent'] = @user_agent 41 | req['Authorization'] = ISUCARI_API_TOKEN 42 | 43 | http = Net::HTTP.new(uri.host, uri.port) 44 | http.use_ssl = uri.scheme == 'https' 45 | res = http.start { http.request(req) } 46 | 47 | if res.code != '200' 48 | raise Error, "status code #{res.code}; body #{res.body}" 49 | end 50 | 51 | JSON.parse(res.body) 52 | end 53 | 54 | def shipment_request(shipment_url, param) 55 | uri = URI.parse("#{shipment_url}/request") 56 | 57 | req = Net::HTTP::Post.new(uri.path) 58 | req.body = param.to_json 59 | req['Content-Type'] = 'application/json' 60 | req['User-Agent'] = @user_agent 61 | req['Authorization'] = ISUCARI_API_TOKEN 62 | 63 | http = Net::HTTP.new(uri.host, uri.port) 64 | http.use_ssl = uri.scheme == 'https' 65 | res = http.start { http.request(req) } 66 | 67 | if res.code != '200' 68 | raise Error, "status code #{res.code}; body #{res.body}" 69 | end 70 | 71 | res.body 72 | end 73 | 74 | def shipment_status(shipment_url, param) 75 | uri = URI.parse("#{shipment_url}/status") 76 | 77 | req = Net::HTTP::Post.new(uri.path) 78 | req.body = param.to_json 79 | req['Content-Type'] = 'application/json' 80 | req['User-Agent'] = @user_agent 81 | req['Authorization'] = ISUCARI_API_TOKEN 82 | 83 | http = Net::HTTP.new(uri.host, uri.port) 84 | http.use_ssl = uri.scheme == 'https' 85 | res = http.start { http.request(req) } 86 | 87 | if res.code != '200' 88 | raise Error, "status code #{res.code}; body #{res.body}" 89 | end 90 | 91 | JSON.parse(res.body) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /isucari/webapp/frontend/src/actions/postShippedDoneAction.ts: -------------------------------------------------------------------------------- 1 | import AppClient from '../httpClients/appClient'; 2 | import { ThunkAction, ThunkDispatch } from 'redux-thunk'; 3 | import { Action } from 'redux'; 4 | import { ErrorRes, ShipDoneReq, ShipDoneRes } from '../types/appApiTypes'; 5 | import { fetchItemAction } from './fetchItemAction'; 6 | import { AppResponseError } from '../errors/AppResponseError'; 7 | import { AppState } from '../index'; 8 | import { SnackBarAction } from './actionTypes'; 9 | 10 | export const POST_SHIPPED_DONE_START = 'POST_SHIPPED_DONE_START'; 11 | export const POST_SHIPPED_DONE_SUCCESS = 'POST_SHIPPED_DONE_SUCCESS'; 12 | export const POST_SHIPPED_DONE_FAIL = 'POST_SHIPPED_DONE_FAIL'; 13 | export type PostShippedDoneActions = 14 | | PostShippedDoneStartAction 15 | | PostShippedDoneSuccessAction 16 | | PostShippedDoneFailAction; 17 | type ThunkResult = ThunkAction< 18 | R, 19 | AppState, 20 | undefined, 21 | PostShippedDoneActions 22 | >; 23 | 24 | export function postShippedDoneAction(itemId: number): ThunkResult { 25 | return (dispatch: ThunkDispatch) => { 26 | Promise.resolve() 27 | .then(() => { 28 | dispatch(postShippedDoneStartAction()); 29 | }) 30 | .then(() => { 31 | return AppClient.post('/ship_done', { 32 | item_id: itemId, 33 | } as ShipDoneReq); 34 | }) 35 | .then(async (response: Response) => { 36 | if (response.status !== 200) { 37 | const errRes: ErrorRes = await response.json(); 38 | throw new AppResponseError(errRes.error, response); 39 | } 40 | 41 | return await response.json(); 42 | }) 43 | .then((body: ShipDoneRes) => { 44 | dispatch(postShippedDoneSuccessAction()); 45 | }) 46 | .then(() => { 47 | dispatch(fetchItemAction(itemId.toString())); // FIXME: 異常系のハンドリングが取引ページ向けでない 48 | }) 49 | .catch((err: Error) => { 50 | dispatch(postShippedDoneFailAction(err.message)); 51 | }); 52 | }; 53 | } 54 | 55 | export interface PostShippedDoneStartAction 56 | extends Action {} 57 | 58 | export function postShippedDoneStartAction(): PostShippedDoneStartAction { 59 | return { 60 | type: POST_SHIPPED_DONE_START, 61 | }; 62 | } 63 | 64 | export interface PostShippedDoneSuccessAction 65 | extends Action {} 66 | 67 | export function postShippedDoneSuccessAction(): PostShippedDoneSuccessAction { 68 | return { 69 | type: POST_SHIPPED_DONE_SUCCESS, 70 | }; 71 | } 72 | 73 | export interface PostShippedDoneFailAction 74 | extends SnackBarAction {} 75 | 76 | export function postShippedDoneFailAction( 77 | error: string, 78 | ): PostShippedDoneFailAction { 79 | return { 80 | type: POST_SHIPPED_DONE_FAIL, 81 | snackBarMessage: error, 82 | variant: 'error', 83 | }; 84 | } 85 | --------------------------------------------------------------------------------