diff --git a/bin/bootstrap.sh b/bin/bootstrap.sh index 398c526219..263f615d0a 100755 --- a/bin/bootstrap.sh +++ b/bin/bootstrap.sh @@ -6,6 +6,7 @@ set -ev # Runs the following tasks in order: # - Install dependencies # - Build `@zooniverse/react-components` +# - Build `@zooniverse/lib-user` # - Build `@zooniverse/lib-classifier` @@ -16,6 +17,10 @@ printf 'Building `lib-react-components`...\n' yarn workspace @zooniverse/react-components install --frozen-lockfile printf '\n' +printf 'Building `lib-user`...\n' +yarn workspace @zooniverse/user install --frozen-lockfile +printf '\n' + printf 'Building `lib-classifier`...\n' yarn workspace @zooniverse/classifier install --frozen-lockfile printf '\n' diff --git a/bin/bootstrap:es6.sh b/bin/bootstrap:es6.sh index 29da7431cf..83ea8d71ad 100755 --- a/bin/bootstrap:es6.sh +++ b/bin/bootstrap:es6.sh @@ -6,6 +6,7 @@ set -ev # Runs the following tasks in order: # - Install dependencies # - Build `@zooniverse/react-components` +# - Build `@zooniverse/lib-user` # - Build `@zooniverse/lib-classifier` @@ -20,6 +21,10 @@ printf 'Building `lib-react-components`...\n' yarn workspace @zooniverse/react-components build:es6 printf '\n' +printf 'Building `lib-user`...\n' +yarn workspace @zooniverse/user build:es6 +printf '\n' + printf 'Building `lib-classifier`...\n' yarn workspace @zooniverse/classifier build:es6 printf '\n' diff --git a/docs/arch/README.md b/docs/arch/README.md index 090fa8fea0..628bcdc814 100644 --- a/docs/arch/README.md +++ b/docs/arch/README.md @@ -54,3 +54,4 @@ - [ADR 52: User Stats Page](adr-52.md) - [ADR 53: Logged-In User Homepage](adr-53.md) - [ADR 54: app-root](adr-54.md) +- [ADR 55: lib-user](adr-55.md) diff --git a/docs/arch/adr-51.md b/docs/arch/adr-51.md index f3c446e750..d4fc9d8a40 100644 --- a/docs/arch/adr-51.md +++ b/docs/arch/adr-51.md @@ -30,7 +30,7 @@ Create export member statistics to CSV functionality for applicable user groups. ## Status -Proposed +Accepted ## Consequences diff --git a/docs/arch/adr-52.md b/docs/arch/adr-52.md index abebabf65b..da85f78603 100644 --- a/docs/arch/adr-52.md +++ b/docs/arch/adr-52.md @@ -24,7 +24,7 @@ Create PDF generation feature to document user contributions. ## Status -Proposed +Accepted ## Consequences diff --git a/docs/arch/adr-53.md b/docs/arch/adr-53.md index 8a78f34ad2..fa959591d6 100644 --- a/docs/arch/adr-53.md +++ b/docs/arch/adr-53.md @@ -24,7 +24,7 @@ Refactor recent classifications cards for redesigned logged-in user homepage. ## Status -Proposed +Accepted ## Consequences diff --git a/docs/arch/adr-55.md b/docs/arch/adr-55.md new file mode 100644 index 0000000000..cde38fc641 --- /dev/null +++ b/docs/arch/adr-55.md @@ -0,0 +1,21 @@ +# ADR 55: lib-user + +October 2023 + +## Context + +We've decided to build a user stats page ([ADR 52](adr-52.md)), a user group stats page ([ADR 51](adr-51.md)), and a logged-in user homepage ([ADR 53](adr-53.md)). The components for these pages will be built in a new "user library" - `lib-user`. This is in accordance with the decision to build app-root ([ADR 54](adr-54.md)). The app-root will serve the homepage and handle traffic for the domain root, https://zooniverse.org, including `/users` stats pages and `/groups` stats pages, utilizing Next.js 13's App Router and by importing components from `lib-user`. + +## Decision + +Create a new library `lib-user` to house the components for the user stats page, user group stats page, and logged-in user homepage. The components will be based on the [related Figma designs](https://www.figma.com/file/qbqbmR3t5XV6eKcpuRj7mG/Group-Stats) and will utilize the Zooniverse stats service [ERAS](https://github.com/zooniverse/eras) (Enhanced Running Average Stats Service) and [Panoptes](https://github.com/zooniverse/panoptes) resources. + +Initially, the components created in `lib-user` will be related to the user stats page, user group stats page, and logged-in user homepage. Subsequently, additional user related components can be added to `lib-user` as needed (for user profile, collections, settings, etc.). + +## Status + +Proposed + +## Consequences + +Create `lib-user` and components noted. diff --git a/packages/lib-user/.babelrc b/packages/lib-user/.babelrc new file mode 100644 index 0000000000..00e4b83163 --- /dev/null +++ b/packages/lib-user/.babelrc @@ -0,0 +1,37 @@ +{ + "plugins": [ + ["@babel/plugin-transform-runtime", { + "helpers": false + }], + ["module-resolver", { + "alias": { + "@components": "./src/components", + "@hooks": "./src/hooks", + "@utils": "./src/utils" + } + }] + ], + "presets": [ + ["@babel/preset-react", { + "runtime": "automatic" + }], + ["@babel/preset-env", { + "modules": "auto" + }] + ], + "env": { + "es6": { + "presets": [ + ["@babel/preset-react", { + "runtime": "automatic" + }], + ["@babel/preset-env", { + "modules": false, + "targets": { + "esmodules": true + } + }] + ] + } + } +} diff --git a/packages/lib-user/.gitignore b/packages/lib-user/.gitignore new file mode 100644 index 0000000000..9d0b71a3c7 --- /dev/null +++ b/packages/lib-user/.gitignore @@ -0,0 +1,2 @@ +build +dist diff --git a/packages/lib-user/README.md b/packages/lib-user/README.md new file mode 100644 index 0000000000..b312af129a --- /dev/null +++ b/packages/lib-user/README.md @@ -0,0 +1,55 @@ +# Zooniverse User and User Group Library + +A library for the Zooniverse user stats, user group stats, and home pages. The components in this library are intended to be imported into app-root as part of `zooniverse.org/users` and `zooniverse.org/groups`. For example, the UserStats component from this library will be imported into the app-root page at `zooniverse.org/users/[login]/stats`. + + + +## Run + +### Node/yarn +```sh +yarn dev +# yarn storybook +``` + +Starts a development server on port 8080 ~~and a Storybook on port 6006~~ by default. + +Use `yarn dev` to run a small development environment app at `localhost:8080`. + +- a staging user stats page can be loaded by query param: `https://localhost:8080?users=[login]/stats` +- a staging user group stats page can be loaded by query param: `https://localhost:8080?groups=[user group ID]` + +Note: query params are used for local development work, but are not used in production. The production urls related to this library are: + +- `https://www.zooniverse.org/users/[login]/stats` +- `https://www.zooniverse.org/users/[login]/stats/certificate` +- `https://www.zooniverse.org/groups/[user group ID]` + + + + + +### Technologies + +- @zooniverse/panoptes-js - Panoptes API javascript client +- @zooniverse/react-components - Zooniverse common React components +- @zooniverse/grommet-theme - Zooniverse brand Grommet theme +- [React.js](https://reactjs.org/) - Component, virtual DOM based javascript library +- [Grommet](https://v2.grommet.io/components) - React UI component library +- [styled-components](https://www.styled-components.com/) - CSS in JS styling library. diff --git a/packages/lib-user/dev/components/App/App.js b/packages/lib-user/dev/components/App/App.js new file mode 100644 index 0000000000..331df6caad --- /dev/null +++ b/packages/lib-user/dev/components/App/App.js @@ -0,0 +1,132 @@ +import oauth from 'panoptes-client/lib/oauth.js' +import { useEffect, useState } from 'react' + +import { GroupStats, UserStats } from '@components/index.js' + +function App ({ + groups = null, + users = null +}) { + const [loading, setLoading] = useState(false) + const [userAuth, setUserAuth] = useState(null) + + useEffect(() => { + async function initAuthorization () { + setLoading(true) + + try { + const userAuth = await oauth.init('357ac7e0e17f6d9b05587477ca98fdb69d70181e674be8e20142e1df97a84d2d') + setUserAuth(userAuth) + setLoading(false) + history.replaceState(null, document.title, location.pathname + location.search) + } catch (error) { + console.error(error) + setLoading(false) + } + } + + initAuthorization() + }, []) + + const login = () => oauth.signIn(window.location.origin) + const logout = () => oauth.signOut().then(setUserAuth) + + let content = ( +
+

Key Components - urls (zooniverse.org/...)

+ +
+ ) + + if (groups) { + const subpaths = groups.split('/') + + if (subpaths[0] === '[user_group_id]') { + content =

In the url query param ?groups=, please replace [user_group_id] with a user group id.

+ } else if (subpaths[1] === 'contributors') { + content =

Group contributors component goes here.

+ } else { + const groupID = subpaths[0] || '' + + content = ( + + ) + } + } + + if (users) { + const subpaths = users.split('/') + + if (subpaths[0] === '[login]') { + content =

In the url query param ?users=, please replace [login] with a user login.

+ } else if (subpaths[1] === 'stats') { + const login = subpaths[0] || '' + + content = ( + + ) + } + } + + return ( +
+
+

lib-user

+ {userAuth ? ( + + ) : ( + + )} +
+ {loading ? +

Loading...

: ( +
+ {content} +
+ )} +
+ ) +} + +export default App + +// Other key users components - urls (zooniverse.org/...): +//
  • favorites (public) - users/[login]/favorites
  • +//
  • collections (public) - users/[login]/collections
  • +//
  • comments (public) - users/[login]/comments
  • +//
  • groups (private) - users/[login]/groups
  • +//
  • projects (private) - users/[login]/projects
  • diff --git a/packages/lib-user/dev/components/App/index.js b/packages/lib-user/dev/components/App/index.js new file mode 100644 index 0000000000..5b2a5782ba --- /dev/null +++ b/packages/lib-user/dev/components/App/index.js @@ -0,0 +1 @@ +export { default } from './App.js' diff --git a/packages/lib-user/dev/index.html b/packages/lib-user/dev/index.html new file mode 100644 index 0000000000..85654f8b58 --- /dev/null +++ b/packages/lib-user/dev/index.html @@ -0,0 +1,10 @@ + + + + + lib-user dev app + + +
    + + diff --git a/packages/lib-user/dev/index.js b/packages/lib-user/dev/index.js new file mode 100644 index 0000000000..361c7a45fb --- /dev/null +++ b/packages/lib-user/dev/index.js @@ -0,0 +1,26 @@ +import { createRoot } from 'react-dom/client' + +import App from './components/App/index.js' + +function getQueryParams() { + if (window.location) { + const url = new URL(window.location) + const { searchParams } = url + const queryParams = {} + searchParams.forEach((value, key) => { + queryParams[key] = value + }) + return queryParams + } + + return {} +} + +const { groups, users } = getQueryParams() +const root = createRoot(document.getElementById('root')) +root.render( + +) diff --git a/packages/lib-user/package.json b/packages/lib-user/package.json new file mode 100644 index 0000000000..45ef7e89ab --- /dev/null +++ b/packages/lib-user/package.json @@ -0,0 +1,59 @@ +{ + "name": "@zooniverse/user", + "description": "A library for Zooniverse user and user group pages", + "license": "Apache-2.0", + "author": "Zooniverse (https://www.zooniverse.org/)", + "version": "0.0.0", + "main": "dist/cjs/index.js", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, + "./*": { + "import": "./dist/esm/*/index.js", + "require": "./dist/cjs/*/index.js" + } + }, + "sideEffects": false, + "type": "module", + "repository": "https://github.com/zooniverse/front-end-monorepo.git", + "bugs": "https://github.com/zooniverse/front-end-monorepo/issues", + "scripts": { + "build": "yarn build:cjs && yarn build:es6", + "build:cjs": "babel ./src/ --out-dir ./dist/cjs --copy-files --no-copy-ignored", + "build:es6": "BABEL_ENV=es6 babel ./src/ --out-dir ./dist/esm --copy-files --no-copy-ignored", + "dev": "BABEL_ENV=es6 webpack serve --config webpack.dev.js", + "prepare": "yarn build", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": {}, + "peerDependencies": { + "@zooniverse/panoptes-js": "~0.4.0" + }, + "devDependencies": { + "@babel/cli": "~7.22.5", + "@babel/core": "~7.22.0", + "@babel/plugin-transform-runtime": "~7.22.0", + "@babel/preset-env": "~7.22.0", + "@babel/preset-react": "~7.22.0", + "@babel/register": "~7.22.5", + "babel-loader": "~9.1.0", + "babel-plugin-module-resolver": "~5.0.0", + "babel-plugin-transform-imports": "~2.0.0", + "css-loader": "~6.8.1", + "html-webpack-plugin": "~5.5.0", + "panoptes-client": "~5.5.1", + "process": "~0.11.10", + "prop-types": "^15.8.1", + "react": "~18.2.0", + "react-dom": "~18.2.0", + "style-loader": "~3.3.1", + "webpack": "~5.88.0", + "webpack-cli": "~5.1.0", + "webpack-dev-server": "~4.15.0" + }, + "engines": { + "node": ">=18.13" + } +} diff --git a/packages/lib-user/src/components/GroupStats/GroupStats.js b/packages/lib-user/src/components/GroupStats/GroupStats.js new file mode 100644 index 0000000000..39d1528f3b --- /dev/null +++ b/packages/lib-user/src/components/GroupStats/GroupStats.js @@ -0,0 +1,32 @@ +'use client' + +import { string } from 'prop-types' + +import { + useGroupStats, + usePanoptesUserGroup +} from '@hooks/index.js' + +function GroupStats ({ + authClient, + groupID +}) { + const { data: group, error, isLoading: groupLoading } = usePanoptesUserGroup(authClient, groupID) + const { data: groupStats, error: groupStatsError, isLoading: groupStatsLoading } = useGroupStats({ authClient, groupID }) + + return ( +
    +

    Hi group with ID {groupID}! 🙌

    +

    Your group display_name is {group?.display_name}.

    +

    Here are your group stats:

    +
    {JSON.stringify(groupStats, null, 2)}
    +
    + ) +} + +GroupStats.propTypes = { + // authClient: object.isRequired, + groupID: string.isRequired +} + +export default GroupStats diff --git a/packages/lib-user/src/components/GroupStats/index.js b/packages/lib-user/src/components/GroupStats/index.js new file mode 100644 index 0000000000..b1a1573e88 --- /dev/null +++ b/packages/lib-user/src/components/GroupStats/index.js @@ -0,0 +1 @@ +export { default } from './GroupStats.js' diff --git a/packages/lib-user/src/components/UserStats/UserStats.js b/packages/lib-user/src/components/UserStats/UserStats.js new file mode 100644 index 0000000000..d7fac2ea4f --- /dev/null +++ b/packages/lib-user/src/components/UserStats/UserStats.js @@ -0,0 +1,33 @@ +'use client' + +import { string } from 'prop-types' + +import { + usePanoptesUser, + useUserStats +} from '@hooks/index.js' + +function UserStats ({ + authClient, + login = '' +}) { + const { data: user, error, isLoading: userLoading } = usePanoptesUser(authClient) + const userID = userLoading ? undefined : (user?.id || null) + const { data: userStats, error: userStatsError, isLoading: userStatsLoading } = useUserStats({ authClient, userID }) + + return ( +
    +

    Hello User with login {login}! 👋

    +

    Your display_name is {user?.display_name}, ID is {user?.id}

    +

    Here are your user stats:

    +
    {JSON.stringify(userStats, null, 2)}
    +
    + ) +} + +UserStats.propTypes = { + // authClient: object.isRequired, + login: string +} + +export default UserStats diff --git a/packages/lib-user/src/components/UserStats/index.js b/packages/lib-user/src/components/UserStats/index.js new file mode 100644 index 0000000000..7a2cfeac2a --- /dev/null +++ b/packages/lib-user/src/components/UserStats/index.js @@ -0,0 +1 @@ +export { default } from './UserStats.js' diff --git a/packages/lib-user/src/components/index.js b/packages/lib-user/src/components/index.js new file mode 100644 index 0000000000..421c1f25c2 --- /dev/null +++ b/packages/lib-user/src/components/index.js @@ -0,0 +1,3 @@ +export { default as GroupStats } from './GroupStats/index.js' + +export { default as UserStats } from './UserStats/index.js' diff --git a/packages/lib-user/src/hooks/index.js b/packages/lib-user/src/hooks/index.js new file mode 100644 index 0000000000..19f1831ac5 --- /dev/null +++ b/packages/lib-user/src/hooks/index.js @@ -0,0 +1,4 @@ +export { default as useGroupStats } from './useGroupStats.js' +export { default as usePanoptesUser } from './usePanoptesUser.js' +export { default as usePanoptesUserGroup } from './usePanoptesUserGroup.js' +export { default as useUserStats } from './useUserStats.js' diff --git a/packages/lib-user/src/hooks/useGroupStats.js b/packages/lib-user/src/hooks/useGroupStats.js new file mode 100644 index 0000000000..c076c347e4 --- /dev/null +++ b/packages/lib-user/src/hooks/useGroupStats.js @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react' + +import { getBearerToken } from '@utils/index.js' + +// TODO: refactor with SWR + +export default function useGroupStats({ authClient, groupID }) { + const [error, setError] = useState(null) + const [groupStats, setGroupStats] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(function () { + async function fetchGroupStats() { + setLoading(true) + setGroupStats(null) + + try { + const authorization = await getBearerToken(authClient) + const headers = { authorization } + const response = await fetch(`https://eras-staging.zooniverse.org/classifications/user_groups/${groupID}?individual_stats_breakdown=true`, { headers }) + const data = await response.json() + if (!ignore) { + setGroupStats(data) + } + } catch (error) { + setError(error) + } + + setLoading(false) + } + + let ignore = false + if (groupID) { + fetchGroupStats(authClient, groupID) + } + return () => { + ignore = true + } + }, [authClient, groupID]) + + return { data: groupStats, error, isLoading: loading } +} diff --git a/packages/lib-user/src/hooks/usePanoptesUser.js b/packages/lib-user/src/hooks/usePanoptesUser.js new file mode 100644 index 0000000000..456d570a78 --- /dev/null +++ b/packages/lib-user/src/hooks/usePanoptesUser.js @@ -0,0 +1,51 @@ +import { auth } from '@zooniverse/panoptes-js' +import { useEffect, useState } from 'react' + +import { getBearerToken } from '@utils/index.js' + +async function fetchPanoptesUser(authClient) { + try { + const authorization = await getBearerToken(authClient) + if (authorization) { + const { user, error } = await auth.decodeJWT(authorization) + if (user) { + return user + } + if (error) { + throw error + } + } + return await authClient.checkCurrent() + } catch (error) { + console.log(error) + return null + } +} + +export default function usePanoptesUser(authClient) { + const [error, setError] = useState(null) + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(function () { + async function checkUserSession() { + setLoading(true) + try { + const panoptesUser = await fetchPanoptesUser(authClient) + setUser(panoptesUser) + } catch (error) { + setError(error) + } + setLoading(false) + } + + checkUserSession() + authClient.listen('change', checkUserSession) + + return function () { + authClient?.stopListening('change', checkUserSession) + } + }, [authClient]) + + return { data: user, error, isLoading: loading } +} diff --git a/packages/lib-user/src/hooks/usePanoptesUserGroup.js b/packages/lib-user/src/hooks/usePanoptesUserGroup.js new file mode 100644 index 0000000000..9a355f825d --- /dev/null +++ b/packages/lib-user/src/hooks/usePanoptesUserGroup.js @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react' +import { panoptes } from '@zooniverse/panoptes-js' + +import { getBearerToken } from '@utils/index.js' + +// TODO: refactor with SWR + +async function fetchPanoptesUserGroup(authClient, groupID) { + try { + const authorization = await getBearerToken(authClient) + const response = await panoptes.get(`/user_groups`, { id: groupID }, { authorization }) + const [userGroup] = response.body.user_groups + return userGroup + } catch (error) { + console.log(error) + return null + } +} + +export default function usePanoptesUserGroup(authClient, groupID) { + const [error, setError] = useState(null) + const [userGroup, setUserGroup] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(function () { + async function handleUserGroup() { + setLoading(true) + try { + const panoptesUserGroup = await fetchPanoptesUserGroup(authClient, groupID) + if (!ignore) { + setUserGroup(panoptesUserGroup) + } + } catch (error) { + setError(error) + } + setLoading(false) + } + + let ignore = false + if (groupID) { + handleUserGroup() + } + return function () { + ignore = true + } + }, [authClient, groupID]) + + return { data: userGroup, error, isLoading: loading } +} diff --git a/packages/lib-user/src/hooks/useUserStats.js b/packages/lib-user/src/hooks/useUserStats.js new file mode 100644 index 0000000000..b544bb46d2 --- /dev/null +++ b/packages/lib-user/src/hooks/useUserStats.js @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react' + +import { getBearerToken } from '@utils/index.js' + +// TODO: refactor with SWR + +export default function useUserStats({ authClient, userID }) { + const [error, setError] = useState(null) + const [userStats, setUserStats] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(function () { + async function fetchUserStats() { + setLoading(true) + setUserStats(null) + + try { + const authorization = await getBearerToken(authClient) + const headers = { authorization } + const response = await fetch(`https://eras-staging.zooniverse.org/classifications/users/${userID}?period=week`, { headers }) + const data = await response.json() + if (!ignore) { + setUserStats(data) + } + } catch (error) { + setError(error) + } + + setLoading(false) + } + + let ignore = false + if (userID) { + fetchUserStats(authClient, userID) + } + return () => { + ignore = true + } + }, [authClient, userID]) + + return { data: userStats, error, isLoading: loading } +} diff --git a/packages/lib-user/src/index.js b/packages/lib-user/src/index.js new file mode 100644 index 0000000000..a344965a59 --- /dev/null +++ b/packages/lib-user/src/index.js @@ -0,0 +1,12 @@ +// components +export { default as UserStats } from './components/UserStats/index.js' +export { default as GroupStats } from './components/GroupStats/index.js' + +// hooks +export { default as useGroupStats } from './hooks/useGroupStats.js' +export { default as usePanoptesUser } from './hooks/usePanoptesUser.js' +export { default as usePanoptesUserGroup } from './hooks/usePanoptesUserGroup.js' +export { default as useUserStats } from './hooks/useUserStats.js' + +// utils +export { default as getBearerToken } from './utils/getBearerToken.js' diff --git a/packages/lib-user/src/utils/getBearerToken.js b/packages/lib-user/src/utils/getBearerToken.js new file mode 100644 index 0000000000..c1c64bd50a --- /dev/null +++ b/packages/lib-user/src/utils/getBearerToken.js @@ -0,0 +1,21 @@ +export default async function getBearerToken (authClient) { + let token = '' + if (authClient) { + try { + const authToken = await authClient.checkBearerToken() + // API method between PJC's auth and oauth clients are inconsistent. + // First party auth client returns a string of the bearer token + // Oauth client returns an object of the bearer token, expiration, and token type + if (typeof authToken === 'string' && authToken) token = authToken + + if (authToken && authToken.access_token) token = authToken.access_token + + if (token) return `Bearer ${token}` + return token + } catch (error) { + console.error('Cannot check bearer token:', error) + } + } + + return token +} diff --git a/packages/lib-user/src/utils/index.js b/packages/lib-user/src/utils/index.js new file mode 100644 index 0000000000..788bd11c62 --- /dev/null +++ b/packages/lib-user/src/utils/index.js @@ -0,0 +1 @@ +export { default as getBearerToken } from './getBearerToken.js' diff --git a/packages/lib-user/webpack.dev.js b/packages/lib-user/webpack.dev.js new file mode 100644 index 0000000000..32d0374f50 --- /dev/null +++ b/packages/lib-user/webpack.dev.js @@ -0,0 +1,87 @@ +import { execSync } from 'child_process' +import HtmlWebpackPlugin from 'html-webpack-plugin' +import path from 'path' +import webpack from 'webpack' + +function gitCommit() { + try { + const commitHash = execSync('git describe --always').toString('utf8').trim() + return commitHash + } catch (error) { + console.log(error) + return 'Not a git repository.' + } +} + +const __dirname = path.dirname(new URL(import.meta.url).pathname) + +const EnvironmentWebpackPlugin = new webpack.EnvironmentPlugin({ + COMMIT_ID: gitCommit(), + DEBUG: false, + NODE_ENV: 'development', + PANOPTES_ENV: 'staging' +}) + +const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ + template: './dev/index.html', + filename: 'index.html', + inject: 'body' +}) + +export default { + devServer: { + allowedHosts: [ + 'bs-local.com', + 'localhost', + '.zooniverse.org' + ], + server: 'https' + }, + entry: [ + './dev/index.js' + ], + mode: 'development', + resolve: { + alias: { + '@components': path.resolve(__dirname, 'src/components'), + '@hooks': path.resolve(__dirname, 'src/hooks'), + '@utils': path.resolve(__dirname, 'src/utils') + }, + fallback: { + url: false, + } + }, + module: { + rules: [ + { + test: /\.js?$/, + exclude: /node_modules/, + use: [{ + loader: 'babel-loader', + options: { compact: false } + }] + }, + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader' + ] + } + ] + }, + output: { + path: path.resolve('build'), + filename: 'main.js', + library: '@zooniverse/user', + libraryTarget: 'umd', + umdNamedDefine: true + }, + plugins: [ + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + EnvironmentWebpackPlugin, + HtmlWebpackPluginConfig + ] +}