Skip to content

Commit

Permalink
Merge pull request #6958 from fjordllc/feature/show-users-divided-by-…
Browse files Browse the repository at this point in the history
…area

都道府県別ユーザー一覧の追加
  • Loading branch information
komagata authored Mar 11, 2024
2 parents 630b3c0 + 6208661 commit 09c2725
Show file tree
Hide file tree
Showing 37 changed files with 657 additions and 105 deletions.
8 changes: 8 additions & 0 deletions app/controllers/api/users/areas_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class API::Users::AreasController < API::BaseController
def index
tokyo_area_id = '13'
@users = Area.users(params[:region], params[:area] || tokyo_area_id)
end
end
7 changes: 7 additions & 0 deletions app/controllers/users/areas_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class Users::AreasController < ApplicationController
def index
@number_of_users_by_region = Area.number_of_users_by_region
end
end
4 changes: 2 additions & 2 deletions app/javascript/components/Companies.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function Companies({ target }) {
return (
<div className="page-body">
<div className="container is-lg">
<div className="card-list a-card is-loading">
<div className="card-list is-loading">
<LoadingUsersPageCompaniesPlaceholder />
</div>
</div>
Expand All @@ -26,7 +26,7 @@ export default function Companies({ target }) {
return (
<div className="page-body">
<div className="container is-lg">
<div className="card-list a-card">
<div className="card-list">
{data.map((company) => (
<Company key={company.id} company={company} />
))}
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/components/Company.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function Company({ company }) {
}

return (
<div className="user-group">
<div className="a-card user-group">
<UserGroupHeader company={company} />
<UserIcons users={company.users} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function LoadingUsersPageCompanyPlaceholder() {
const userIconCount = 16

return (
<div className="user-group">
<div className="a-card user-group">
<header className="user-group__header">
<h2 className="group-company-name">
<span className="group-company-name__link">
Expand Down
102 changes: 102 additions & 0 deletions app/javascript/components/Users/FilterByArea.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react'
// components
import LoadingListPlaceholder from '../LoadingListPlaceholder'
import EmptyMessage from '../ui/EmptyMessage'
import { UserGroup } from '../ui/UserGroup'
import { MultiColumns } from '../layout/MultiColumns'
// hooks
import { useSearchParams, usePopstate } from '../../hooks/useSearchParams'
import useSWR from 'swr'
// helper
import fetcher from '../../fetcher'

function RegionCard({ region, numberOfUsersByRegion, onUpdateSelectedArea }) {
return (
<nav className="page-nav a-card">
<header className="page-nav__header">
<h2 className="page-nav__title">
<span className="page-nav__title-inner">{region}</span>
</h2>
</header>
<hr className="a-border-tint"></hr>
<ul className="page-nav__items">
{Object.keys(numberOfUsersByRegion).map((area) => (
<li key={area} className="page-nav__item">
<button
onClick={() => onUpdateSelectedArea({ region, area })}
className="page-nav__item-link a-text-link">
{`${area}${numberOfUsersByRegion[area]})`}
</button>
</li>
))}
</ul>
</nav>
)
}

/**
* 都道府県を指定しないデフォルトでは東京都が選択されます
*/
export default function FilterByArea({ numberOfUsersByRegion }) {
const { searchParams, setSearchParams } = useSearchParams({ area: '東京都' })
const apiUrl = '/api/users/areas?'
const { data: users, error, mutate } = useSWR(apiUrl + searchParams, fetcher)

const handleUpdateSelectedArea = async ({ region, area }) => {
const search = new URLSearchParams({ region, area })
const newUsers = await fetcher(apiUrl + search).catch((error) => {
console.error(error)
})
mutate(newUsers)
setSearchParams(search)
}

usePopstate(async () => {
const newUsers = await fetcher(apiUrl + searchParams).catch((error) => {
console.error(error)
})
mutate(newUsers)
})

if (error) return <>エラーが発生しました。</>
if (!users) {
return (
<div className="page-body">
<div className="container is-md">
<LoadingListPlaceholder />
</div>
</div>
)
}

return (
<MultiColumns data-testid="areas" isReverse>
{/* region毎に区分されたareaの選択一覧 */}
<MultiColumns.Sub className="is-sm">
{Object.keys(numberOfUsersByRegion).map((region) => (
<RegionCard
key={region}
region={region}
numberOfUsersByRegion={numberOfUsersByRegion[region]}
onUpdateSelectedArea={handleUpdateSelectedArea}
/>
))}
</MultiColumns.Sub>
{/* 選択されたareaのユーザー一覧 */}
<MultiColumns.Main>
<section className="a-card">
{users.length > 0 ? (
<UserGroup>
<UserGroup.Header>
{searchParams.get('area') || '東京都'}
</UserGroup.Header>
<UserGroup.Icons users={users} />
</UserGroup>
) : (
<EmptyMessage>都道府県別ユーザー一覧はありません</EmptyMessage>
)}
</section>
</MultiColumns.Main>
</MultiColumns>
)
}
38 changes: 38 additions & 0 deletions app/javascript/components/layout/MultiColumns.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'
import clsx from 'clsx'

export const MultiColumns = ({
className,
children,
isReverse = false,
...props
}) => {
return (
<div className={clsx('page-body', className)} {...props}>
<div className="container is-lg">
<div className={clsx('page-body__columns', isReverse && 'is-reverse')}>
{children}
</div>
</div>
</div>
)
}

const MultiColumnsMain = ({ className, children, ...props }) => {
return (
<div className={clsx('page-body__column is-main', className)} {...props}>
{children}
</div>
)
}

const MultiColumnsSub = ({ className, children, ...props }) => {
return (
<div className={clsx('page-body__column is-sub', className)} {...props}>
{children}
</div>
)
}

MultiColumns.Main = MultiColumnsMain
MultiColumns.Sub = MultiColumnsSub
12 changes: 12 additions & 0 deletions app/javascript/components/ui/EmptyMessage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'

export default function EmptyMessage({ children }) {
return (
<div className="o-empty-message">
<div className="o-empty-message__icon">
<i className="fa-regular fa-smile" />
</div>
<p className="o-empty-message__text">{children}</p>
</div>
)
}
46 changes: 46 additions & 0 deletions app/javascript/components/ui/UserGroup.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'
import clsx from 'clsx'

export const UserGroup = ({ className, ...props }) => {
return <div className={clsx('user-group', className)} {...props} />
}

const UserGroupHeader = ({ className, children, ...props }) => {
return (
<header className={clsx('user-group__header', className)} {...props}>
<h2 className="user-group__title">{children}</h2>
</header>
)
}

const UserGroupIcons = ({ users, className, ...props }) => {
return (
<div className={clsx('a-user-icons', className)} {...props}>
<div className="a-user-icons__items">
{users.map((user) => (
<UserIcon user={user} key={user.id} />
))}
</div>
</div>
)
}

const UserIcon = ({ user }) => {
const primaryRole = `is-${user.primary_role}`

return (
<a className="a-user-icons__item-link" href={user.url}>
<span className={clsx('a-user-role', primaryRole)}>
<img
src={user.avatar_url}
title={user.icon_title}
data-login-name={user.login_name}
className="a-user-icons__item-icon a-user-icon"
/>
</span>
</a>
)
}

UserGroup.Header = UserGroupHeader
UserGroup.Icons = UserGroupIcons
2 changes: 1 addition & 1 deletion app/javascript/courses-practices.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
:learnings='learnings',
:currentUser='currentUser')
.page-body__column.is-sub
nav.page-nav
nav.page-nav.a-card
ul.page-nav__items
li.page-nav__item(
v-for='category in containsPractices',
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/elapsed_days.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template lang="pug">
nav.page-body__column.is-sub
.page-nav
.page-nav.a-card
ol.page-nav__items.elapsed-days
li.page-nav__item.is-reply-deadline(
:class='activeClass(countProductsByElapsedDays(7))')
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/generation.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template lang="pug">
.user-group.is-loading(v-if='!loaded')
.a-card.user-group.is-loading(v-if='!loaded')
loadingGenerationsPageGenerationPlaceholder
.user-group(v-else-if='users.length !== 0')
.a-card.user-group(v-else-if='users.length !== 0')
header.user-group__header
h2.user-group__title
a.user-group__title-link(:href='generation_url')
Expand Down
11 changes: 5 additions & 6 deletions app/javascript/generations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@
p.o-empty-message__text
| ユーザーはありません
.container.is-lg(v-else)
.card-list.a-card
generation(
v-for='generation in generations',
:key='generation.number',
:generation='generation',
:target='target')
generation(
v-for='generation in generations',
:key='generation.number',
:generation='generation',
:target='target')
nav.pagination(v-if='totalPages > 1')
pager(v-bind='pagerProps')
</template>
Expand Down
52 changes: 52 additions & 0 deletions app/javascript/hooks/useLocation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// React18にアップデートしたらshimを使うのを辞めてください
import { useSyncExternalStore } from 'use-sync-external-store/shim'

// Proxyを使ってhistoryのメソッド呼び出しに合わせてイベントを発行する処理を挟みます
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy
// Navigation APIには対応していません
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API
function proxyHistoryMethod(method) {
const handler = {
// applyの関数は関数呼び出し時に実行されます
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply
apply: function (target, thisArg, argumentsList) {
const event = new Event(method.toLowerCase())
window.dispatchEvent(event)
return Reflect.apply(target, thisArg, argumentsList)
}
}

history[method] = new Proxy(history[method], handler)
}

// historyのメソッドをproxyに置き換える
proxyHistoryMethod('pushState')
proxyHistoryMethod('replaceState')

function subscribe(callback) {
window.addEventListener('pushstate', callback)
window.addEventListener('replacestate', callback)
window.addEventListener('popstate', callback)
return () => {
window.removeEventListener('pushstate', callback)
window.removeEventListener('replacestate', callback)
window.removeEventListener('popstate', callback)
}
}

/**
* URLが変わる度に再レンダリングを起こして新しいlocationを返すhookです
* レンダリングされる瞬間にlocationを使う(locationの内容が表示に関係している)
* 場合に使って下さい
* @param {(location) => any} [selector=null] - locationの一部分だけ使いたい場合は関数を渡すことも出来ます
* @example
* location.pathnameのみ欲しい場合の例
* const pathname = useLocation((location) => location.pathname)
*
* @see 参考 https://ja.react.dev/learn/lifecycle-of-reactive-effects#can-global-or-mutable-values-be-dependencies
*/
function useLocation(selector = (location) => location) {
return useSyncExternalStore(subscribe, () => selector(location))
}

export { useLocation }
Loading

0 comments on commit 09c2725

Please sign in to comment.