diff --git a/app/frontend/images/social-icons.svg b/app/frontend/images/social-icons.svg new file mode 100644 index 000000000..33e34089c --- /dev/null +++ b/app/frontend/images/social-icons.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index cdfa9a495..f1c42e4f0 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -9,6 +9,7 @@ import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item'; interface FormInputProps extends FormComponent, AbstractFormItemProps { icon?: ReactNode, addOn?: ReactNode, + addOnAction?: (event: React.MouseEvent) => void, addOnClassName?: string, debounce?: number, type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week' | 'hidden', @@ -21,7 +22,7 @@ interface FormInputProps extends FormComponent({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce }: FormInputProps) => { +export const FormInput = ({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnAction, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce }: FormInputProps) => { /** * Debounced (ie. temporised) version of the 'on change' callback. */ @@ -65,7 +66,7 @@ export const FormInput = ({ id, re disabled={disabled} readOnly={readOnly} placeholder={placeholder} /> - {addOn && {addOn}} + {addOn && {addOn}} ); }; diff --git a/app/frontend/src/javascript/components/socials/edit-socials.tsx b/app/frontend/src/javascript/components/socials/edit-socials.tsx new file mode 100644 index 000000000..8681f6c43 --- /dev/null +++ b/app/frontend/src/javascript/components/socials/edit-socials.tsx @@ -0,0 +1,67 @@ +import React, { useState, useReducer } from 'react'; +import { UseFormRegister, UseFormResetField } from 'react-hook-form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { User } from '../../models/user'; +import { SocialNetwork } from '../../models/social-network'; +import Icons from '../../../../images/social-icons.svg'; +import { FormInput } from '../form/form-input'; +import { Trash } from 'phosphor-react'; +import { useTranslation } from 'react-i18next'; + +interface EditSocialsProps { + register: UseFormRegister, + resetField: UseFormResetField, + networks: SocialNetwork[], +} + +export const EditSocials = ({ register, resetField, networks }: EditSocialsProps) => { + const { t } = useTranslation('shared'); + + const initSelectedNetworks = networks.filter(el => el.url !== ''); + const [selectedNetworks, setSelectedNetworks] = useState(initSelectedNetworks); + const selectNetwork = (network) => { + setSelectedNetworks([...selectedNetworks, network]); + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'delete': + setSelectedNetworks(selectedNetworks.filter(el => el !== action.payload.network)); + resetField(action.payload.field); + return state.map(el => el === action.payload.network + ? { ...el, url: '' } + : el); + case 'update': + return state.map(el => el === action.payload + ? { ...el, url: action.payload.url } + : el); + default: + return state; + } + }; + const [userNetworks, dispatch] = useReducer(reducer, networks); + + return ( + <> + + {userNetworks.map((network, index) => + !selectedNetworks.includes(network) && selectNetwork(network)}> + )} + + + {userNetworks.map((network, index) => + selectedNetworks.includes(network) && + } + addOn={} + addOnAction={() => dispatch({ type: 'delete', payload: { network, field: `profile.${network.name}` } })} /> + )} + + > + ); +}; diff --git a/app/frontend/src/javascript/components/socials/socials.tsx b/app/frontend/src/javascript/components/socials/socials.tsx new file mode 100644 index 000000000..f70ef52dd --- /dev/null +++ b/app/frontend/src/javascript/components/socials/socials.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { SocialNetwork } from '../../models/social-network'; +import Icons from '../../../../images/social-icons.svg'; +import { IApplication } from '../../models/application'; +import { Loader } from '../base/loader'; +import { react2angular } from 'react2angular'; + +declare const Application: IApplication; + +interface SocialsProps { + networks: SocialNetwork[] +} + +const plop = [{ + name: 'facebook', + url: 'https://plop.com' +}, { + name: 'linkedin', + url: 'https://plop.com' +}]; + +export const Socials: React.FC = ({ networks = plop }) => { + return ( + + {networks.map((network, index) => + + + + )} + + ); +}; + +const SocialsWrapper: React.FC = (props) => { + return ( + + + + ); +}; +Application.Components.component('socials', react2angular(SocialsWrapper, ['networks'])); diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 398294cb9..4fdf0cc98 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -11,6 +11,8 @@ import { GenderInput } from './gender-input'; import { ChangePassword } from './change-password'; import Switch from 'react-switch'; import { PasswordInput } from './password-input'; +import { EditSocials } from '../socials/edit-socials'; +import UserLib from '../../lib/user'; declare const Application: IApplication; @@ -25,7 +27,7 @@ interface UserProfileFormProps { export const UserProfileForm: React.FC = ({ action, size, user, className, onError }) => { const { t } = useTranslation('shared'); - const { handleSubmit, register, control, formState } = useForm({ defaultValues: { ...user } }); + const { handleSubmit, register, resetField, control, formState } = useForm({ defaultValues: { ...user } }); const output = useWatch({ control }); const [isOrganization, setIsOrganization] = React.useState(user.invoicing_profile.organization !== null); @@ -37,6 +39,8 @@ export const UserProfileForm: React.FC = ({ action, size, console.log(action, data); }; + const userNetworks = new UserLib(user).getSocialNetworks(user); + return ( @@ -104,6 +108,12 @@ export const UserProfileForm: React.FC = ({ action, size, currentFormPassword={output.password} formState={formState} />} + + {t('app.shared.user_profile_form.account_networks')} + + {t('app.shared.user_profile_form.organization_data')} diff --git a/app/frontend/src/javascript/lib/user.ts b/app/frontend/src/javascript/lib/user.ts index ca773dbfd..6121f1ac4 100644 --- a/app/frontend/src/javascript/lib/user.ts +++ b/app/frontend/src/javascript/lib/user.ts @@ -19,4 +19,17 @@ export default class UserLib { return false; }; + + /** + * Filter social networks from the user's profile + */ + getSocialNetworks = (customer: User): {name: string, url: string, active: boolean}[] => { + const userNetworks = []; + const supportedNetworks = ['facebook', 'twitter', 'viadeo', 'linkedin', 'instagram', 'youtube', 'vimeo', 'dailymotion', 'github', 'echosciences', 'pinterest', 'lastfm', 'flickr']; + + for (const [name, url] of Object.entries(customer.profile)) { + supportedNetworks.includes(name) && userNetworks.push({ name, url }); + } + return userNetworks; + }; } diff --git a/app/frontend/src/javascript/models/social-network.ts b/app/frontend/src/javascript/models/social-network.ts new file mode 100644 index 000000000..a6cbd07df --- /dev/null +++ b/app/frontend/src/javascript/models/social-network.ts @@ -0,0 +1,4 @@ +export interface SocialNetwork { + name: string, + url: string +} diff --git a/app/frontend/src/javascript/typings/import-svg.d.ts b/app/frontend/src/javascript/typings/import-svg.d.ts new file mode 100644 index 000000000..f4f138dee --- /dev/null +++ b/app/frontend/src/javascript/typings/import-svg.d.ts @@ -0,0 +1,5 @@ +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value: any; + export default value; +} diff --git a/app/frontend/src/stylesheets/app.layout.scss b/app/frontend/src/stylesheets/app.layout.scss index 285ce6039..23c3adace 100644 --- a/app/frontend/src/stylesheets/app.layout.scss +++ b/app/frontend/src/stylesheets/app.layout.scss @@ -566,17 +566,34 @@ body.container { // profile edition -- add a social network buttons .social-icons { - & > div { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 1.6rem; + & > * { cursor: pointer; padding: 0.2em; width: 3em; - display: inline-block; - text-align: center; + height: 3em; + display: flex; + justify-content: center; + align-items: center; border-radius: 3px; - border: 1px solid transparent; - + overflow: hidden; + } + & > img { + border: 1px solid var(--gray-soft-dark); + background-color: var(--gray-soft-lightest); &:hover { - border: 1px solid $border-color; + border-color: var(--secondary); + background-color: var(--secondary); + } + } + & > a { + &:hover { box-shadow: 0 0 0 1px var(--main); } + img { + max-width: 100%; + height: inherit; } } } diff --git a/app/frontend/src/stylesheets/modules/form/form-item.scss b/app/frontend/src/stylesheets/modules/form/form-item.scss index 412723f17..844476705 100644 --- a/app/frontend/src/stylesheets/modules/form/form-item.scss +++ b/app/frontend/src/stylesheets/modules/form/form-item.scss @@ -58,6 +58,7 @@ border: 1px solid var(--gray-soft-dark); border-radius: var(--border-radius); transition: border-color ease-in-out 0.15s; + overflow: hidden; .icon, .addon { @@ -71,12 +72,24 @@ .icon { grid-area: icon; border-right: 1px solid var(--gray-soft-dark); + & > * { + max-width: 24px; + max-height: 24px; + } + } + + .addon { + grid-area: addon; + border-left: 1px solid var(--gray-soft-dark); + &.is-btn:hover { + cursor: pointer; + filter: brightness(90%); + } } & > input { grid-area: field; border: none; - border-radius: var(--border-radius); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .08); padding: 0 0.8rem; color: var(--gray-hard-darkest); @@ -123,11 +136,6 @@ color: var(--gray-soft-darkest); } } - - .addon { - grid-area: addon; - border-left: 1px solid var(--gray-soft-dark); - } } &.is-incorrect &-field { border-color: var(--error); diff --git a/app/frontend/templates/shared/about.html b/app/frontend/templates/shared/about.html index ef0fd8ab1..3d586dec7 100644 --- a/app/frontend/templates/shared/about.html +++ b/app/frontend/templates/shared/about.html @@ -20,6 +20,7 @@ {{ 'app.public.about.privacy_policy' }} + diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 688bdc9db..730cc5946 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -37,6 +37,7 @@ en: add_an_avatar: "Add an avatar" personal_data: "Personal" account_data: "Account" + account_networks: "Social networks" organization_data: "Organization" declare_organization: "I am an organization" pseudonym: "Nickname" diff --git a/config/webpack/modules/svg.js b/config/webpack/modules/svg.js new file mode 100644 index 000000000..9d90623bc --- /dev/null +++ b/config/webpack/modules/svg.js @@ -0,0 +1,4 @@ +module.exports = { + test: /\.svg$/i, + type: 'asset' +}; diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index f4a533c26..b1d47fc2f 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -9,6 +9,7 @@ const sassErb = require('./modules/sass_erb'); const html = require('./modules/html'); const uiTour = require('./modules/ui-tour'); const hmr = require('./modules/hmr'); +const svg = require('./modules/svg'); const isDevelopment = process.env.NODE_ENV !== 'production'; @@ -44,7 +45,8 @@ const customConfig = { html, sass, uiTour, - hmr + hmr, + svg ] }, resolve: { diff --git a/db/schema.rb b/db/schema.rb index 65c866ce9..7540a2baa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do enable_extension "unaccent" create_table "abuses", id: :serial, force: :cascade do |t| - t.integer "signaled_id" t.string "signaled_type" + t.integer "signaled_id" t.string "first_name" t.string "last_name" t.string "email" @@ -49,8 +49,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do t.string "locality" t.string "country" t.string "postal_code" - t.integer "placeable_id" t.string "placeable_type" + t.integer "placeable_id" t.datetime "created_at" t.datetime "updated_at" end @@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do end create_table "assets", id: :serial, force: :cascade do |t| - t.integer "viewable_id" t.string "viewable_type" + t.integer "viewable_id" t.string "attachment" t.string "type" t.datetime "created_at" @@ -146,8 +146,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do end create_table "credits", id: :serial, force: :cascade do |t| - t.integer "creditable_id" t.string "creditable_type" + t.integer "creditable_id" t.integer "plan_id" t.integer "hours" t.datetime "created_at" @@ -369,15 +369,15 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do create_table "notifications", id: :serial, force: :cascade do |t| t.integer "receiver_id" - t.integer "attached_object_id" t.string "attached_object_type" + t.integer "attached_object_id" t.integer "notification_type_id" t.boolean "is_read", default: false t.datetime "created_at" t.datetime "updated_at" t.string "receiver_type" t.boolean "is_send", default: false - t.jsonb "meta_data", default: {} + t.jsonb "meta_data", default: "{}" t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id" end @@ -423,6 +423,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do t.boolean "send_scope_to_token_endpoint" t.string "post_logout_redirect_uri" t.string "uid_field" + t.string "extra_authorize_params" + t.string "allow_authorize_params" t.string "client__identifier" t.string "client__secret" t.string "client__redirect_uri" @@ -570,8 +572,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do create_table "prices", id: :serial, force: :cascade do |t| t.integer "group_id" t.integer "plan_id" - t.integer "priceable_id" t.string "priceable_type" + t.integer "priceable_id" t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -681,8 +683,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.integer "reservable_id" t.string "reservable_type" + t.integer "reservable_id" t.integer "nb_reserve_places" t.integer "statistic_profile_id" t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id" @@ -691,8 +693,8 @@ ActiveRecord::Schema.define(version: 2022_04_25_095244) do create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.integer "resource_id" t.string "resource_type" + t.integer "resource_id" t.datetime "created_at" t.datetime "updated_at" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
{{ 'app.public.about.privacy_policy' }}