Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8691

【第4回】「みんなのポートフォリオまとめサイト」を作ります~フロントエンド編~ 【Cover】成果物URL: https://minna.itsumen.com

$
0
0

ワイ 「この記事のカバーです」
https://qiita.com/kiwatchi1991/items/58b53c5b4ddf8d6a7053

バックナンバー

【第1回】「みんなのポートフォリオまとめサイト」を作ります~プロトタイプ作成編~

【第2回】「みんなのポートフォリオまとめサイト」を作ります~REST API編~

【第3回】「みんなのポートフォリオまとめサイト」を作ります~SNSログイン編~

成果物

https://minna.itsumen.com

リポジトリ

フロントエンド

https://github.com/yuzuru2/minna_frontend

バックエンド

https://github.com/yuzuru2/minna_backend

部品

store router

src/index.tsx
import*asReactfrom'react';import{render}from'react-dom';import{Provider}from'react-redux';import{Route,Switch}from'react-router';import{ConnectedRouter,connectRouter}from'connected-react-router';import{createStore,combineReducers}from'redux';import{createBrowserHistory}from'history';import'bootstrap/dist/css/bootstrap.min.css';import'babel-polyfill';importinitfrom'src/firebase';// reducerimportredcuer_basicfrom'src/reducer/basic';importredcuer_pagingfrom'src/reducer/paging';importredcuer_productfrom'src/reducer/product';importredcuer_profilefrom'src/reducer/profile';// componentimportinfofrom'src/component/info';importtypefrom'src/component/type';importsearchfrom'src/component/search';importprofilefrom'src/component/profile';consthistory=createBrowserHistory();exportconststore=createStore(combineReducers({router:connectRouter(history),basic:redcuer_basic,paging:redcuer_paging,product:redcuer_product,profile:redcuer_profile,}));constid=setInterval(()=>{if(!store.getState().basic.firebaseInitFlag){return;}render(<Providerstore={store}><ConnectedRouterhistory={history}><Switch><Routeexactpath={'/'}component={info}/>
<Routeexactpath={'/info/:num'}component={info}/>
<Routeexactpath={'/type/:id/:num'}component={type}/>
<Routeexactpath={'/search/:id/:num'}component={search}/>
<Routeexactpath={'/profile/:id/:num'}component={profile}/>
</Switch>
</ConnectedRouter>
</Provider>,
document.getElementById('app'));clearInterval(id);},250);// ブラウザバックでリロードwindow.addEventListener('popstate',e=>{window.location.reload();});init();

reducer

src/reducer/basic.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={uid:null,loadingFlag:true,firebaseInitFlag:false,};exportdefault(state=initialState,action:i_reducdr['basic'])=>{switch(action.type){caseConstant.reducer.basic.uid:return{...state,uid:action.uid};caseConstant.reducer.basic.loadingFlag:return{...state,loadingFlag:action.loadingFlag};caseConstant.reducer.basic.firebaseInitFlag:return{...state,firebaseInitFlag:action.firebaseInitFlag};default:returnstate;}};
src/reducer/paging.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={count:0,list:[],};exportdefault(state=initialState,action:i_reducdr['paging'])=>{switch(action.type){caseConstant.reducer.paging.count:return{...state,count:action.count};caseConstant.reducer.paging.list:return{...state,list:action.list};default:returnstate;}};
src/reducer/product.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={ptype:0,id:'',title:'',url:'',repo:'',};exportdefault(state=initialState,action:i_reducdr['product'])=>{switch(action.type){caseConstant.reducer.product.ptype:return{...state,ptype:action.ptype};caseConstant.reducer.product.id:return{...state,id:action.id};caseConstant.reducer.product.title:return{...state,title:action.title};caseConstant.reducer.product.url:return{...state,url:action.url};caseConstant.reducer.product.repo:return{...state,repo:action.repo};default:returnstate;}};
src/reducer/profile.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={name:'',twitterUrl:'',githubUrl:'',};exportdefault(state=initialState,action:i_reducdr['profile'])=>{switch(action.type){caseConstant.reducer.profile.name:return{...state,name:action.name};caseConstant.reducer.profile.twitterUrl:return{...state,twitterUrl:action.twitterUrl};caseConstant.reducer.profile.githubUrl:return{...state,githubUrl:action.githubUrl};default:returnstate;}};

action

src/action/basic.ts
importConstantfrom'src/constant';constaction={uid:(uid:string)=>{return{type:Constant.reducer.basic.uid,uid:uid};},loadingFlag:(loadingFlag:boolean)=>{return{type:Constant.reducer.basic.loadingFlag,loadingFlag:loadingFlag,};},firebaseInitFlag:(firebaseInitFlag:boolean)=>{return{type:Constant.reducer.basic.firebaseInitFlag,firebaseInitFlag:firebaseInitFlag,};},};exportdefaultaction;
src/action/paging.ts
importConstantfrom'src/constant';importi_reducerfrom'src/interface/reducer';constaction={count:(count:number)=>{return{type:Constant.reducer.paging.count,count:count};},list:(list:i_reducer['paging']['list'])=>{return{type:Constant.reducer.paging.list,list:list};},};exportdefaultaction;
src/action/product.ts
importConstantfrom'src/constant';constaction={ptype:(ptype:number)=>{return{type:Constant.reducer.product.ptype,ptype:ptype};},id:(id:string)=>{return{type:Constant.reducer.product.id,id:id};},title:(title:string)=>{return{type:Constant.reducer.product.title,title:title};},url:(url:string)=>{return{type:Constant.reducer.product.url,url:url};},repo:(repo:string)=>{return{type:Constant.reducer.product.repo,repo:repo};},};exportdefaultaction;
src/action/profile.ts
importConstantfrom'src/constant';constaction={name:(name:string)=>{return{type:Constant.reducer.profile.name,name:name};},twitterUrl:(twitterUrl:string)=>{return{type:Constant.reducer.profile.twitterUrl,twitterUrl:twitterUrl,};},githubUrl:(githubUrl:string)=>{return{type:Constant.reducer.profile.githubUrl,githubUrl:githubUrl,};},};exportdefaultaction;

component

src/component/info/index.ts
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/info/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importLoginButtonfrom'src/component/util/loginButton';importModalfrom'src/component/util/productPost';importSearchfrom'src/component/util/search';importPaginationfrom'src/component/info/pagination';importTypefrom'src/component/util/type';// logicimportfirstfrom'src/logic/info/first';exportdefaultconnect(store=>store)(()=>{constparams:{num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return<Loaging/>;}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}>{store.getState().basic.uid!==null?<Modal/>:<LoginButton/>}<Type/></div>
<divstyle={{textAlign:'center',marginTop:15}}><Search/></div>
<mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/info/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';exportdefault()=>{returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">全投稿</li>
<liclassName="breadcrumb-item active">{store.getState().paging.count}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/info/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={_pageNum}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/info/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/profile/index.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/profile/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importPaginationfrom'src/component/profile/pagination';importUserfrom'src/component/profile/user';importfirstfrom'src/logic/profile/first';exportdefaultconnect(store=>store)(()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(params.id,_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return<Loaging/>;}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}><User/><hr/></div>
<br/><mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/profile/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';exportdefault()=>{returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">ユーザ</li>
<liclassName="breadcrumb-item active">{store.getState().profile.name}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/profile/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={_pageNum}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/profile/${params.id}/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/profile/user.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{Modal,Button}from'react-bootstrap';importcopyfrom'copy-to-clipboard';import{store}from'src';importactionfrom'src/action/profile';importlogicfrom'src/logic/profile/userUpdate';exportdefault()=>{constparams:{id:string}=useParams();const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-secondary"onClick={()=>setshowModal(true)}>プロフィール</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>プロフィール</h5>
</Modal.Header>
<Modal.Body><divclassName="form-group"><label>ニックネーム</label>
<inputreadOnly={store.getState().basic.uid!==params.id}className="form-control"placeholder="ニックネーム"maxLength={15}value={store.getState().profile.name}onChange={e=>store.dispatch(action.name(e.target.value))}/>
</div>
<divclassName="form-group"><label>GitHub</label>
<inputtype="url"readOnly={store.getState().basic.uid!==params.id}className="form-control"placeholder="GithubのURL"maxLength={100}value={store.getState().profile.githubUrl}onChange={e=>store.dispatch(action.githubUrl(e.target.value))}onClick={()=>{if(store.getState().basic.uid!==params.id){copy(store.getState().profile.githubUrl);alert('コピーしました');}}}/>
</div>
<divclassName="form-group"><label>Twitter</label>
<inputtype="url"readOnly={store.getState().basic.uid!==params.id}className="form-control"placeholder="TwitterのURL"maxLength={100}value={store.getState().profile.twitterUrl}onChange={e=>store.dispatch(action.twitterUrl(e.target.value))}onClick={()=>{if(store.getState().basic.uid!==params.id){copy(store.getState().profile.twitterUrl);alert('コピーしました');}}}/>
</div>
</Modal.Body>
<Modal.Footer>{store.getState().basic.uid===params.id?(<ButtononClick={async()=>awaitlogic()}>変更</Button>
):('')}</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().profile)]);};
src/component/search/index.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/search/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importLoginButtonfrom'src/component/util/loginButton';importModalfrom'src/component/util/productPost';importSearchfrom'src/component/util/search';importPaginationfrom'src/component/search/pagination';importTypefrom'src/component/util/type';importfirstfrom'src/logic/search/first';exportdefaultconnect(store=>store)(()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(params.id,_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return(<><Loaging/></>
);}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}>{store.getState().basic.uid!==null?<Modal/>:<LoginButton/>}<Type/></div>
<divstyle={{textAlign:'center',marginTop:15}}><Search/></div>
<mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/search/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';import{useParams}from'react-router-dom';exportdefault()=>{constparams:{id:string}=useParams();returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">検索</li>
<liclassName="breadcrumb-item active">{params.id}</li>
<liclassName="breadcrumb-item active">{store.getState().paging.count}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/search/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{id:string;num:string}=useParams();returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={Number(params.num)}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/search/${params.id}/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/type/index.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/type/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importLoginButtonfrom'src/component/util/loginButton';importModalfrom'src/component/util/productPost';importSearchfrom'src/component/util/search';importPaginationfrom'src/component/type/pagination';importTypefrom'src/component/util/type';importfirstfrom'src/logic/type/first';exportdefaultconnect(store=>store)(()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(params.id,_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return(<><Loaging/></>
);}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}>{store.getState().basic.uid!==null?<Modal/>:<LoginButton/>}<Type/></div>
<divstyle={{textAlign:'center',marginTop:15}}><Search/></div>
<mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/type/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';importConstantfrom'src/constant';import{useParams}from'react-router-dom';exportdefault()=>{constparams:{id:string}=useParams();returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">タイプ</li>
<liclassName="breadcrumb-item active">{Constant.type[params.id]}</li>
<liclassName="breadcrumb-item active">{store.getState().paging.count}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/type/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{id:string;num:string}=useParams();returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={Number(params.num)}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/type/${params.id}/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/util/aside.tsx
import*asReactfrom'react';exportdefault()=>{returnReact.useMemo(()=>{return(<><aside><divstyle={{textAlign:'right'}}><ahref="https://www.youtube.com/channel/UCuRrjmWcjASMgl5TqHS02AQ/videos"><imgsrc="/img/1.jpg"width="70%"/></a>
<br/><br/><ahref="https://twitter.com/yuzuru_program"><imgsrc="/img/2.jpg"width="70%"/></a>
<br/><br/><ahref="https://yuzuru.itsumen.com"><imgsrc="/img/3.jpg"width="70%"/></a>
<br/><br/><ahref="https://code.itsumen.com"><imgsrc="/img/4.jpg"width="70%"/></a>
<br/><br/><ahref="https://board.itsumen.com"><imgsrc="/img/5.jpg"width="70%"/></a>
<br/><br/><ahref="https://nuxtchat.itsumen.com"><imgsrc="/img/6.jpg"width="70%"/></a>
<br/><br/></div>
</aside>
</>
);},[location.href]);};
src/component/util/header.tsx
import*asReactfrom'react';importConstantfrom'src/constant';import{store}from'src';import{Navbar}from'react-bootstrap';importLoginButtonfrom'src/component/util/loginButton';importLogoutButtonfrom'src/component/util/logoutButton';exportdefault()=>{returnReact.useMemo(()=>{return(<><headerstyle={{background:'rgb(255, 141, 153)'}}><Navbarexpand={false}><ahref="/"style={{fontSize:15,color:'#fff'}}>{Constant.tile}</a>
<Navbar.Togglearia-controls="basic-navbar-nav"aria-expanded="false"/><Navbar.Collapse><ulclassName="nav navbar-nav ml-auto">{store.getState().basic.uid===null?(<listyle={{textAlign:'center'}}><LoginButton/></li>
):(<><br/><listyle={{textAlign:'center'}}><buttontype="button"className="btn btn-info"onClick={()=>(location.href=`/profile/${store.getState().basic.uid}/1`)}>マイページ</button>
</li>
<br/><listyle={{textAlign:'center'}}><LogoutButton/></li>
</>
)}</ul>
</Navbar.Collapse>
</Navbar>
</header>
</>
);},[store.getState().basic.uid]);};
src/component/util/loading.tsx
import*asReactfrom'react';exportdefault()=>{return(<><div><divclassName="position-absolute h-100 w-100 m-0 d-flex align-items-center justify-content-center"><divclassName="spinner-border text-primary"role="status"><spanclassName="sr-only">Loading...</span>
</div>
</div>
</div>
</>
);};
src/component/util/loginButton.tsx
import*asReactfrom'react';import{store}from'src';importlogicfrom'src/logic/util/loginButton';exportdefault()=>{returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-primary"onClick={()=>logic()}>ログイン</button>
</>
);},[store.getState().basic.uid]);};
src/component/util/logoutButton.tsx
import*asReactfrom'react';import{store}from'src';importlogicfrom'src/logic/util/logoutButton';exportdefault()=>{returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-danger"onClick={()=>logic()}>ログアウト</button>
</>
);},[store.getState().basic.uid]);};
src/component/util/pagingList.tsx
import*asReactfrom'react';importmomentfrom'moment';moment.locale('ja');importConstantfrom'src/constant';import{store}from'src';importi_reducerfrom'src/interface/reducer';importProductUpdatefrom'src/component/util/productUpdate';importlogicfrom'src/logic/util/productDelete';exportdefault(params:{page:React.SFC})=>{returnReact.useMemo(()=>{constlist:i_reducer['paging']['list']=store.getState().paging.list;return(<><section><ul>{list.map((m,i)=>{return(<listyle={{paddingLeft:15,marginTop:15}}key={i}><divclassName="card"><divclassName="card-body">{/* タイトル */}<divclassName="form-group"><label>タイトル</label>
<br/><p>{m.title}</p>
</div>
<hr/>{/* URL */}<divclassName="form-group"><label>URL</label>
<br/><ahref={m.url}>{m.url}</a>
</div>
<hr/>{/* タイプ */}<divclassName="form-group"><label>タイプ</label>
<br/><ahref={`${location.protocol}//${location.host}/type/${m.type}/1`}>{Constant.type[m.type]}</a>
</div>
<hr/>{/* リポジトリ */}<divclassName="form-group"><label>リポジトリ</label>
<br/><ahref={m.repo}>{m.repo}</a>
</div>
<hr/>{/* ニックネーム */}<divclassName="form-group"><label>ニックネーム</label>
<br/><ahref={`/profile/${m.uid}/1`}>{m.name[0]}</a>
</div>
<hr/>{/* 投稿・更新日 */}<divclassName="form-group"style={{textAlign:'right'}}><time>投稿:{''}{moment(m.createdAt).format('YYYY-MM-DD HH:mm:ss')}</time>
<br/><time>更新:{''}{moment(m.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
{/* 編集・削除 */}<divclassName="form-group"style={{textAlign:'right'}}>{store.getState().basic.uid===m.uid?(<><ProductUpdateid={m._id}title={m.title}url={m.url}repo={m.repo}ptype={m.type}/>
<br/><br/><buttontype="button"className="btn btn-danger"onClick={()=>{confirm('削除しますか')?logic(m._id):'';}}>削除</button>
</>
):('')}</div>
</div>
</div>
</li>
);})}</ul>
<params.page/></section>
</>
);},[JSON.stringify(store.getState().paging.list),JSON.stringify(store.getState().product),]);};
src/component/util/productPost.tsx
import*asReactfrom'react';import{Modal,Button}from'react-bootstrap';import{store}from'src';importConstantfrom'src/constant';importactionfrom'src/action/product';importlogicfrom'src/logic/util/productPost';exportdefault()=>{const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-success"onClick={()=>{store.dispatch(action.id(''));store.dispatch(action.title(''));store.dispatch(action.url(''));store.dispatch(action.repo(''));store.dispatch(action.ptype(0));setshowModal(true);}}>投稿</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>投稿</h5>
</Modal.Header>
<Modal.Body><divclassName="form-group"><label>タイトル</label>
<textareaclassName="form-control"rows={3}placeholder="30文字内"maxLength={30}value={store.getState().product.title}onChange={e=>store.dispatch(action.title(e.target.value))}></textarea>
</div>
<divclassName="form-group"><label>URL</label>
<inputtype="url"className="form-control"placeholder="成果物URL"maxLength={100}value={store.getState().product.url}onChange={e=>store.dispatch(action.url(e.target.value))}/>
</div>
<divclassName="form-group"><label>リポジトリ</label>
<inputtype="url"className="form-control"placeholder="リポジトリURL"maxLength={100}value={store.getState().product.repo}onChange={e=>store.dispatch(action.repo(e.target.value))}/>
</div>
<divclassName="form-group"><label>タイプ</label>
<selectclassName="form-control"value={store.getState().product.ptype}onChange={e=>store.dispatch(action.ptype(Number(e.target.value)))}>{Object.keys(Constant.type).map(key=>{return(<optionkey={key}value={key}>{Constant.type[key]}</option>
);})}</select>
</div>
</Modal.Body>
<Modal.Footer><ButtononClick={async()=>confirm('投稿してもよいですか?')?awaitlogic():''}>投稿</Button>
</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().product)]);};
src/component/util/productUpdate.tsx
import*asReactfrom'react';import{Modal,Button}from'react-bootstrap';import{store}from'src';importConstantfrom'src/constant';importactionfrom'src/action/product';importlogicfrom'src/logic/util/productUpdate';exportdefault(params:{id:string;title:string;url:string;repo:string;ptype:number;})=>{const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-success"onClick={()=>{store.dispatch(action.id(params.id));store.dispatch(action.title(params.title));store.dispatch(action.url(params.url));store.dispatch(action.repo(params.repo));store.dispatch(action.ptype(Number(params.ptype)));setshowModal(true);}}>変更</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>変更</h5>
</Modal.Header>
<Modal.Body><divclassName="form-group"><label>タイトル</label>
<textareaclassName="form-control"rows={3}placeholder="30文字内"maxLength={30}value={store.getState().product.title}onChange={e=>store.dispatch(action.title(e.target.value))}></textarea>
</div>
<divclassName="form-group"><label>URL</label>
<inputtype="url"className="form-control"placeholder="成果物URL"maxLength={100}value={store.getState().product.url}onChange={e=>store.dispatch(action.url(e.target.value))}/>
</div>
<divclassName="form-group"><label>リポジトリ</label>
<inputtype="url"className="form-control"placeholder="リポジトリURL"maxLength={100}value={store.getState().product.repo}onChange={e=>store.dispatch(action.repo(e.target.value))}/>
</div>
<divclassName="form-group"><label>タイプ</label>
<selectclassName="form-control"value={store.getState().product.ptype}onChange={e=>store.dispatch(action.ptype(Number(e.target.value)))}>{Object.keys(Constant.type).map(key=>{return(<optionkey={key}value={key}>{Constant.type[key]}</option>
);})}</select>
</div>
</Modal.Body>
<Modal.Footer><ButtononClick={async()=>awaitlogic()}>変更</Button>
</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().product)]);};
src/component/util/search.tsx
import*asReactfrom'react';exportdefault()=>{const[text,setText]=React.useState('');returnReact.useMemo(()=>{return(<><divclassName="mx-auto"style={{maxWidth:'300px'}}><divclassName="input-group"><inputtype="text"className="form-control"placeholder="検索"maxLength={15}onChange={e=>setText(e.target.value)}onKeyPress={e=>{if(e.key=='Enter'){text.length===0?(location.href='/'):(location.href=`${location.protocol}//${location.host}/search/${text}/1`);}}}/>
<divclassName="input-group-append"><buttonclassName="btn btn-info"type="button"onClick={()=>text.length===0?(location.href='/'):(location.href=`${location.protocol}//${location.host}/search/${text}/1`)}><iclassName="fa fa-search"></i>
</button>
</div>
</div>
</div>
</>
);},[text]);};
src/component/util/type.tsx
import*asReactfrom'react';import{Modal,Button}from'react-bootstrap';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-warning"style={{marginLeft:15}}onClick={()=>setshowModal(true)}>ジャンル検索</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>ジャンル検索</h5>
</Modal.Header>
<Modal.Body>{Object.keys(Constant.type).map(key=>{return(<divclassName="form-group"key={key}><ahref={`/type/${key}/1`}>{Constant.type[key]}</a>
</div>
);})}</Modal.Body>
<Modal.Footer><ButtononClick={async()=>setshowModal(false)}>閉じる</Button>
</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().product)]);};###constant```src/constant/index.ts
let reducerCount = 0;

export default class {
  static readonly tile = 'みんなのポートフォリオ';

  static readonly api_url = {
    development: `http://localhost:9000/.netlify/functions/api/v1`,production:`https://eager-spence-3a3400.netlify.app/.netlify/functions/api/v1`,};staticreadonlyreducer={basic:{uid:String(reducerCount++),loadingFlag:String(reducerCount++),firebaseInitFlag:String(reducerCount++),},profile:{name:String(reducerCount++),twitterUrl:String(reducerCount++),githubUrl:String(reducerCount++),},product:{ptype:String(reducerCount++),id:String(reducerCount++),title:String(reducerCount++),url:String(reducerCount++),repo:String(reducerCount++),},paging:{count:String(reducerCount++),list:String(reducerCount++),},};staticreadonlyurl={['/create/friend']:'/create/friend',['/create/user']:'/create/user',['/create/product']:'/create/product',['/paging/all']:'/paging/all',['/paging/title']:'/paging/title',['/paging/type']:'/paging/type',['/paging/user']:'/paging/user',['/update/user']:'/update/user',['/update/product']:'/update/product',['/cancel/friend']:'/cancel/friend',['/cancel/product']:'/cancel/product',['/find/user']:'/find/user',};staticreadonlyfirebase_config={apiKey:'AIzaSyA1Z1xnyt5-cp3YXNcyMzR30a2oh5zWaR4',authDomain:'minna-eee2e.firebaseapp.com',databaseURL:'https://minna-eee2e.firebaseio.com',projectId:'minna-eee2e',storageBucket:'minna-eee2e.appspot.com',messagingSenderId:'861193838038',appId:'1:861193838038:web:97f1254de063da49221877',measurementId:'G-ZYXZCK1RDS',};staticreadonlytype={0:'Webアプリ',1:'スマホアプリ',2:'デスクトップアプリ',3:'スクレイピング',4:'ホムペ',5:'その他',};staticreadonlypagingNum=5;constructor(){thrownewError('new禁止');}}

firebase

src/firebase/index.ts
import*asfirebasefrom'firebase/app';import'firebase/auth';importConstantfrom'src/constant';importactionfrom'src/action/basic';import{store}from'src';firebase.initializeApp(Constant.firebase_config);firebase.auth().onAuthStateChanged(asyncdata=>{if(!store.getState().basic.firebaseInitFlag){store.dispatch(action.firebaseInitFlag(true));}if(data===null){store.dispatch(action.uid(null));return;}store.dispatch(action.uid(data.uid));});exportdefaultasync()=>{try{returnawaitfirebase.auth().currentUser.getIdToken(true);}catch(e){return'';}};

interface

src/interface/reducer.ts
interfacereducer{home:{type:string;name:string;count:number;};basic:{type:string;uid:string;loadingFlag:boolean;firebaseInitFlag:boolean;};profile:{type:string;name:string;twitterUrl:string;githubUrl:string;};product:{type:string;id:string;ptype:number;title:string;url:string;repo:string;};paging:{type:string;count:number;list:{_id:string;type:number;title:string;url:string;repo:string;name:string[];uid:string;createdAt:Date;updatedAt:Date;}[];};}exportdefaultreducer;

logic

src/logic/info/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(num:number)=>{const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/all']}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/profile/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importprofile_actionfrom'src/action/profile';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(id:string,num:number)=>{// プロフィールconst_profile=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/find/user']}/${id}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_profile.status!==200){return;}const_profile_json:i_reducer['profile'][]=await_profile.json();if(_profile_json.length===0){location.href='/';return;}store.dispatch(profile_action.name(_profile_json[0].name));store.dispatch(profile_action.githubUrl(_profile_json[0].githubUrl));store.dispatch(profile_action.twitterUrl(_profile_json[0].twitterUrl));// 投稿const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/user']}/${id}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/profile/userUpdate.ts
importvalidatorfrom'validator';import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// プロフィール変更exportdefaultasync()=>{constvalidate=()=>{if(store.getState().profile.name.length===0){alert('ニックネームが入力されていません');returnfalse;}if(!validator.isURL(store.getState().profile.githubUrl,{protocols:['http','https'],require_protocol:true,})){if(store.getState().profile.githubUrl.length!==0){alert('GitHubのURLが不正です');returnfalse;}}if(!validator.isURL(store.getState().profile.twitterUrl,{protocols:['http','https'],require_protocol:true,})){if(store.getState().profile.twitterUrl.length!==0){alert('TwitterのURLが不正です');returnfalse;}}returntrue;};if(!validate()){return;}const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/update/user']}`,{method:'put',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({name:store.getState().profile.name,githubUrl:store.getState().profile.githubUrl,twitterUrl:store.getState().profile.twitterUrl,}),});if(_res.status!==200){alert('更新に失敗しました');return;}alert('変更しました');location.href=`/profile/${store.getState().basic.uid}/1`;};
src/logic/search/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(id:string,num:number)=>{const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/title']}/${id}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/type/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(id:string,num:number)=>{const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/type']}/${id}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/util/loginButton.ts
import*asReactfrom'react';import*asfirebasefrom'firebase/app';import{store}from'src';importactionfrom'src/action/basic';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// ツイッターログインexportdefault()=>{constprovider=newfirebase.auth.TwitterAuthProvider();firebase.auth().signInWithPopup(provider).then(asyncresult=>{store.dispatch(action.loadingFlag(true));store.dispatch(action.uid(awaitresult.user.uid));const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/create/user']}/`,{method:'post',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({}),});if(_res.status!==200){alert('ログイン失敗');return;}location.reload();});};
src/logic/util/logoutButton.ts
import*asfirebasefrom'firebase/app';import{store}from'src';importactionfrom'src/action/basic';exportdefault()=>{if(!confirm('ログアウトしますか?')){return;}store.dispatch(action.loadingFlag(true));firebase.auth().signOut().then(()=>{location.href='/';}).catch(error=>{alert('エラーが発生しました');});};
src/logic/util/productDelete.ts
import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importactionfrom'src/action/basic';importgetJwtTokenfrom'src/firebase';// 投稿削除exportdefaultasync(id:string)=>{store.dispatch(action.loadingFlag(true));const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/cancel/product']}`,{method:'delete',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({id:id,}),});if(_res.status!==200){alert('削除に失敗しました');return;}alert('削除しました');location.reload();};
src/logic/util/productPost.ts
importvalidatorfrom'validator';import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importactionfrom'src/action/basic';importgetJwtTokenfrom'src/firebase';// 投稿exportdefaultasync()=>{constvalidate=()=>{if(store.getState().product.title.length===0){alert('タイトルが入力されていません');returnfalse;}if(store.getState().product.url.length===0){alert('URLが入力されていません');returnfalse;}if(!validator.isURL(store.getState().product.url,{protocols:['http','https'],require_protocol:true,})){alert('URLが不正です');returnfalse;}if(!validator.isURL(store.getState().product.repo,{protocols:['http','https'],require_protocol:true,})){if(store.getState().product.repo.length!==0){alert('リポジトリURLが不正です');returnfalse;}}returntrue;};store.dispatch(action.loadingFlag(true));if(!validate()){store.dispatch(action.loadingFlag(false));return;}const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/create/product']}`,{method:'post',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({title:store.getState().product.title,url:store.getState().product.url,repo:store.getState().product.repo,type:store.getState().product.ptype,}),});if(_res.status!==200){alert('投稿に失敗しました');return;}location.href='/';};
src/logic/util/productUpdate.ts
importvalidatorfrom'validator';import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// 投稿exportdefaultasync()=>{constvalidate=()=>{if(store.getState().product.title.length===0){alert('タイトルが入力されていません');returnfalse;}if(store.getState().product.url.length===0){alert('タイトルが入力されていません');returnfalse;}if(!validator.isURL(store.getState().product.url,{protocols:['http','https'],require_protocol:true,})){alert('URLが不正です');returnfalse;}if(!validator.isURL(store.getState().product.repo,{protocols:['http','https'],require_protocol:true,})){if(store.getState().product.repo.length!==0){alert('リポジトリURLが不正です');returnfalse;}}returntrue;};if(!validate()){return;}const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/update/product']}`,{method:'put',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({id:store.getState().product.id,title:store.getState().product.title,url:store.getState().product.url,repo:store.getState().product.repo,type:store.getState().product.ptype,}),});if(_res.status!==200){alert('変更に失敗しました');return;}alert('変更しました');location.reload();};

util

src/util/index.ts
exportconstgetHeaders=(jwtToken:string)=>{return{Authorization:jwtToken,'Content-Type':'application/json',};};

css

src/css/style.css
body{margin:0;}/* bootstrapの謎の右の余白を消す */#app{overflow:hidden;}li{list-style:none;}main{background:rgb(255,250,250);}section{width:70%;}aside{width:30%;}/* スマホ用 */@mediascreenand(max-width:768px){section{width:95%;padding-left:0;}sectionul{padding-left:5px;}aside{display:none;width:0%;}}

Viewing all articles
Browse latest Browse all 8691

Trending Articles