Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

都道府県別ユーザー一覧の追加 #6958

Merged
merged 32 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e85da71
useSearchParams.jsの追加
dowdiness Oct 7, 2023
3c37ae5
clsxの追加
dowdiness Oct 7, 2023
5d28d4e
都道府県別ユーザー一覧の追加
dowdiness Oct 7, 2023
b41d075
run prettier
dowdiness Oct 7, 2023
e93cb0f
prettierの実行
dowdiness Oct 7, 2023
9b807c8
prettier修正
dowdiness Oct 7, 2023
4237531
sectionを使うように変更
dowdiness Oct 7, 2023
baa1c10
変数名をprimaryRoleへと変更
dowdiness Oct 7, 2023
56651f5
userモデルの余計な変更を取り消し
dowdiness Oct 7, 2023
4822720
UserGroupだけexportするように書き直し
dowdiness Oct 7, 2023
0307ab6
分かりにくい変数名や関数名を修正
dowdiness Oct 12, 2023
79c8108
簡易なテストの追加
dowdiness Oct 12, 2023
197284a
prettierを実行
dowdiness Oct 12, 2023
fdcfc87
usePopstateを作って使うように変更
dowdiness Oct 20, 2023
ecf1ba6
都道府県別ユーザー一覧の大枠を設定
machida Oct 24, 2023
ff5f81e
page-nav の css を整理
machida Oct 31, 2023
5206daa
userリストのデザインの整理
machida Nov 1, 2023
0e61cb0
:cop:
machida Nov 1, 2023
3776a1d
class名変更に伴うtest変更
machida Nov 1, 2023
068110b
number_of_usersをnumber_of_users_by_regionへと変更
dowdiness Nov 22, 2023
30544a3
areaからregionへと名前を変更
dowdiness Nov 22, 2023
bb4791f
MultiColumnsを追加
dowdiness Nov 22, 2023
a281109
UserListフォルダをUsersへと名前を変更
dowdiness Nov 22, 2023
dd87ec2
run prettier
dowdiness Nov 22, 2023
a2965d5
Regionのシステムテストを改善
dowdiness Nov 22, 2023
4553944
スタイルが当てられていない部分を修正
dowdiness Jan 17, 2024
0959a8b
落ちているテストを修正
dowdiness Jan 17, 2024
2d082af
CSSとHTMLの変更に合わせてテストを元に戻す
dowdiness Jan 19, 2024
134c6ea
regionモデルとsubdivisionOrCountryの名前をareaへと変更する
dowdiness Feb 14, 2024
df2f315
locationにuseSyncExternalStoreを使うように変更
dowdiness Feb 26, 2024
5a63716
AreasController内のマジックナンバーに変数を使って名前を付けた
dowdiness Mar 6, 2024
6208661
不要なrequire 'uri'を削除
dowdiness Mar 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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